webscout 2025.10.15__py3-none-any.whl → 2025.10.17__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/Extra/YTToolkit/README.md +1 -1
- webscout/Extra/tempmail/README.md +3 -3
- webscout/Provider/ClaudeOnline.py +350 -0
- webscout/Provider/OPENAI/README.md +1 -1
- webscout/Provider/TTI/bing.py +4 -4
- webscout/Provider/TTI/claudeonline.py +315 -0
- webscout/__init__.py +1 -1
- webscout/client.py +4 -5
- webscout/litprinter/__init__.py +0 -42
- webscout/scout/README.md +59 -8
- webscout/scout/core/scout.py +62 -0
- webscout/scout/element.py +251 -45
- webscout/search/__init__.py +3 -4
- webscout/search/engines/bing/images.py +5 -2
- webscout/search/engines/bing/news.py +6 -4
- webscout/search/engines/bing/text.py +5 -2
- webscout/search/engines/yahoo/__init__.py +41 -0
- webscout/search/engines/yahoo/answers.py +16 -0
- webscout/search/engines/yahoo/base.py +34 -0
- webscout/search/engines/yahoo/images.py +324 -0
- webscout/search/engines/yahoo/maps.py +16 -0
- webscout/search/engines/yahoo/news.py +258 -0
- webscout/search/engines/yahoo/suggestions.py +140 -0
- webscout/search/engines/yahoo/text.py +273 -0
- webscout/search/engines/yahoo/translate.py +16 -0
- webscout/search/engines/yahoo/videos.py +302 -0
- webscout/search/engines/yahoo/weather.py +220 -0
- webscout/search/http_client.py +1 -1
- webscout/search/yahoo_main.py +54 -0
- webscout/{auth → server}/__init__.py +2 -23
- webscout/server/config.py +84 -0
- webscout/{auth → server}/request_processing.py +3 -28
- webscout/{auth → server}/routes.py +6 -148
- webscout/server/schemas.py +23 -0
- webscout/{auth → server}/server.py +11 -43
- webscout/server/simple_logger.py +84 -0
- webscout/version.py +1 -1
- webscout/version.py.bak +1 -1
- webscout/zeroart/README.md +17 -9
- webscout/zeroart/__init__.py +78 -6
- webscout/zeroart/effects.py +51 -1
- webscout/zeroart/fonts.py +559 -1
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/METADATA +11 -54
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/RECORD +51 -46
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/entry_points.txt +1 -1
- webscout/Extra/weather.md +0 -281
- webscout/auth/api_key_manager.py +0 -189
- webscout/auth/auth_system.py +0 -85
- webscout/auth/config.py +0 -175
- webscout/auth/database.py +0 -755
- webscout/auth/middleware.py +0 -248
- webscout/auth/models.py +0 -185
- webscout/auth/rate_limiter.py +0 -254
- webscout/auth/schemas.py +0 -103
- webscout/auth/simple_logger.py +0 -236
- webscout/search/engines/yahoo.py +0 -65
- webscout/search/engines/yahoo_news.py +0 -64
- /webscout/{auth → server}/exceptions.py +0 -0
- /webscout/{auth → server}/providers.py +0 -0
- /webscout/{auth → server}/request_models.py +0 -0
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/WHEEL +0 -0
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-2025.10.15.dist-info → webscout-2025.10.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for the Webscout API server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import List, Dict, Optional, Any
|
|
7
|
+
from webscout.Litlogger import Logger, LogLevel, LogFormat, ConsoleHandler
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
# Configuration constants
|
|
11
|
+
DEFAULT_PORT = 8000
|
|
12
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
13
|
+
|
|
14
|
+
# Setup logger
|
|
15
|
+
logger = Logger(
|
|
16
|
+
name="webscout.api",
|
|
17
|
+
level=LogLevel.INFO,
|
|
18
|
+
handlers=[ConsoleHandler(stream=sys.stdout)],
|
|
19
|
+
fmt=LogFormat.DEFAULT
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ServerConfig:
|
|
24
|
+
"""Centralized configuration management for the API server."""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.provider_map: Dict[str, Any] = {}
|
|
28
|
+
self.default_provider: str = "ChatGPT"
|
|
29
|
+
self.base_url: Optional[str] = None
|
|
30
|
+
self.host: str = DEFAULT_HOST
|
|
31
|
+
self.port: int = DEFAULT_PORT
|
|
32
|
+
self.debug: bool = False
|
|
33
|
+
self.cors_origins: List[str] = ["*"]
|
|
34
|
+
self.max_request_size: int = 10 * 1024 * 1024 # 10MB
|
|
35
|
+
self.request_timeout: int = 300 # 5 minutes
|
|
36
|
+
self.auth_required: bool = False
|
|
37
|
+
self.rate_limit_enabled: bool = False
|
|
38
|
+
self.request_logging_enabled: bool = os.getenv("WEBSCOUT_REQUEST_LOGGING", "true").lower() == "true" # Enable request logging by default
|
|
39
|
+
|
|
40
|
+
def update(self, **kwargs) -> None:
|
|
41
|
+
"""Update configuration with provided values."""
|
|
42
|
+
for key, value in kwargs.items():
|
|
43
|
+
if hasattr(self, key) and value is not None:
|
|
44
|
+
setattr(self, key, value)
|
|
45
|
+
logger.info(f"Config updated: {key} = {value}")
|
|
46
|
+
|
|
47
|
+
def validate(self) -> None:
|
|
48
|
+
"""Validate configuration settings."""
|
|
49
|
+
if self.port < 1 or self.port > 65535:
|
|
50
|
+
raise ValueError(f"Invalid port number: {self.port}")
|
|
51
|
+
|
|
52
|
+
if self.default_provider not in self.provider_map and self.provider_map:
|
|
53
|
+
available_providers = list(set(v.__name__ for v in self.provider_map.values()))
|
|
54
|
+
logger.warning(f"Default provider '{self.default_provider}' not found. Available: {available_providers}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AppConfig:
|
|
58
|
+
"""Legacy configuration class for backward compatibility."""
|
|
59
|
+
provider_map = {}
|
|
60
|
+
tti_provider_map = {} # Add TTI provider map
|
|
61
|
+
default_provider = "ChatGPT"
|
|
62
|
+
default_tti_provider = "PollinationsAI" # Add default TTI provider
|
|
63
|
+
base_url: Optional[str] = None
|
|
64
|
+
auth_required: bool = False
|
|
65
|
+
rate_limit_enabled: bool = False
|
|
66
|
+
request_logging_enabled: bool = os.getenv("WEBSCOUT_REQUEST_LOGGING", "true").lower() == "true" # Enable request logging by default
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def set_config(cls, **data):
|
|
70
|
+
"""Set configuration values."""
|
|
71
|
+
# Filter out auth-related keys
|
|
72
|
+
auth_keys = {'api_key'}
|
|
73
|
+
filtered_data = {k: v for k, v in data.items() if k not in auth_keys}
|
|
74
|
+
|
|
75
|
+
for key, value in filtered_data.items():
|
|
76
|
+
setattr(cls, key, value)
|
|
77
|
+
# Sync with new config system
|
|
78
|
+
try:
|
|
79
|
+
from .server import get_config
|
|
80
|
+
config = get_config()
|
|
81
|
+
config.update(**filtered_data)
|
|
82
|
+
except ImportError:
|
|
83
|
+
# Handle case where server module is not available
|
|
84
|
+
pass
|
|
@@ -16,8 +16,7 @@ import sys
|
|
|
16
16
|
|
|
17
17
|
from .request_models import Message, ChatCompletionRequest
|
|
18
18
|
from .exceptions import APIError, clean_text
|
|
19
|
-
|
|
20
|
-
from .auth_system import get_auth_components
|
|
19
|
+
|
|
21
20
|
from .simple_logger import log_api_request, get_client_ip, generate_request_id
|
|
22
21
|
from .config import AppConfig
|
|
23
22
|
|
|
@@ -33,9 +32,9 @@ logger = Logger(
|
|
|
33
32
|
async def log_request(request_id: str, ip_address: str, model_used: str, question: str,
|
|
34
33
|
answer: str, response_time_ms: int, status_code: int = 200,
|
|
35
34
|
error_message: str = None, provider: str = None, request_obj=None):
|
|
36
|
-
"""Log API request
|
|
35
|
+
"""Log API request."""
|
|
37
36
|
try:
|
|
38
|
-
# Use simple logger
|
|
37
|
+
# Use simple logger if request logging is enabled
|
|
39
38
|
if AppConfig.request_logging_enabled:
|
|
40
39
|
user_agent = None
|
|
41
40
|
if request_obj:
|
|
@@ -52,30 +51,6 @@ async def log_request(request_id: str, ip_address: str, model_used: str, questio
|
|
|
52
51
|
error=error_message,
|
|
53
52
|
user_agent=user_agent
|
|
54
53
|
)
|
|
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
54
|
except Exception as e:
|
|
80
55
|
logger.error(f"Failed to log request {request_id}: {e}")
|
|
81
56
|
# Don't raise exception to avoid breaking the main request flow
|
|
@@ -12,7 +12,6 @@ from typing import Any
|
|
|
12
12
|
from fastapi import FastAPI, Request, Body, Query
|
|
13
13
|
from fastapi.responses import JSONResponse
|
|
14
14
|
from fastapi.exceptions import RequestValidationError
|
|
15
|
-
from fastapi.security import APIKeyHeader
|
|
16
15
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
17
16
|
from starlette.status import (
|
|
18
17
|
HTTP_422_UNPROCESSABLE_ENTITY,
|
|
@@ -28,7 +27,6 @@ from .request_models import (
|
|
|
28
27
|
ErrorResponse
|
|
29
28
|
)
|
|
30
29
|
from .schemas import (
|
|
31
|
-
APIKeyCreateRequest, APIKeyCreateResponse, APIKeyValidationResponse,
|
|
32
30
|
HealthCheckResponse
|
|
33
31
|
)
|
|
34
32
|
from .exceptions import APIError
|
|
@@ -40,10 +38,9 @@ from .request_processing import (
|
|
|
40
38
|
process_messages, prepare_provider_params,
|
|
41
39
|
handle_streaming_response, handle_non_streaming_response
|
|
42
40
|
)
|
|
43
|
-
from .auth_system import get_auth_components
|
|
44
41
|
from .simple_logger import request_logger
|
|
45
42
|
from ..search import DuckDuckGoSearch, YepSearch
|
|
46
|
-
from
|
|
43
|
+
from ..search import BingSearch
|
|
47
44
|
|
|
48
45
|
# Setup logger
|
|
49
46
|
logger = Logger(
|
|
@@ -59,29 +56,6 @@ class Api:
|
|
|
59
56
|
|
|
60
57
|
def __init__(self, app: FastAPI) -> None:
|
|
61
58
|
self.app = app
|
|
62
|
-
self.get_api_key = APIKeyHeader(name="authorization", auto_error=False)
|
|
63
|
-
|
|
64
|
-
def register_authorization(self):
|
|
65
|
-
"""Register legacy authorization middleware."""
|
|
66
|
-
@self.app.middleware("http")
|
|
67
|
-
async def authorization(request: Request, call_next):
|
|
68
|
-
if AppConfig.api_key is not None:
|
|
69
|
-
auth_header = await self.get_api_key(request)
|
|
70
|
-
path = request.url.path
|
|
71
|
-
if path.startswith("/v1"): # Only protect /v1 routes
|
|
72
|
-
if auth_header is None:
|
|
73
|
-
return JSONResponse(
|
|
74
|
-
status_code=HTTP_401_UNAUTHORIZED,
|
|
75
|
-
content={"error": {"message": "API key required", "type": "authentication_error"}}
|
|
76
|
-
)
|
|
77
|
-
if auth_header.startswith("Bearer "):
|
|
78
|
-
auth_header = auth_header[7:]
|
|
79
|
-
if not secrets.compare_digest(AppConfig.api_key, auth_header):
|
|
80
|
-
return JSONResponse(
|
|
81
|
-
status_code=HTTP_403_FORBIDDEN,
|
|
82
|
-
content={"error": {"message": "Invalid API key", "type": "authentication_error"}}
|
|
83
|
-
)
|
|
84
|
-
return await call_next(request)
|
|
85
59
|
|
|
86
60
|
def register_validation_exception_handler(self):
|
|
87
61
|
"""Register comprehensive exception handlers."""
|
|
@@ -162,7 +136,6 @@ class Api:
|
|
|
162
136
|
"""Register all API routes."""
|
|
163
137
|
self._register_model_routes()
|
|
164
138
|
self._register_chat_routes()
|
|
165
|
-
self._register_auth_routes()
|
|
166
139
|
self._register_websearch_routes()
|
|
167
140
|
self._register_monitoring_routes()
|
|
168
141
|
|
|
@@ -407,105 +380,7 @@ class Api:
|
|
|
407
380
|
"internal_error"
|
|
408
381
|
)
|
|
409
382
|
|
|
410
|
-
def _register_auth_routes(self):
|
|
411
|
-
"""Register authentication routes."""
|
|
412
|
-
# Only register auth endpoints if authentication is required
|
|
413
|
-
if not AppConfig.auth_required:
|
|
414
|
-
logger.info("Auth endpoints are disabled (no-auth mode)")
|
|
415
|
-
return
|
|
416
|
-
auth_components = get_auth_components()
|
|
417
|
-
api_key_manager = auth_components.get("api_key_manager")
|
|
418
383
|
|
|
419
|
-
@self.app.post(
|
|
420
|
-
"/v1/auth/generate-key",
|
|
421
|
-
response_model=APIKeyCreateResponse,
|
|
422
|
-
tags=["Authentication"],
|
|
423
|
-
description="Generate a new API key for a user."
|
|
424
|
-
)
|
|
425
|
-
async def generate_api_key(request: APIKeyCreateRequest = Body(...)):
|
|
426
|
-
"""Generate a new API key."""
|
|
427
|
-
if not api_key_manager:
|
|
428
|
-
raise APIError("Authentication system not initialized", HTTP_500_INTERNAL_SERVER_ERROR)
|
|
429
|
-
|
|
430
|
-
try:
|
|
431
|
-
api_key, user = await api_key_manager.create_api_key(
|
|
432
|
-
username=request.username,
|
|
433
|
-
telegram_id=request.telegram_id,
|
|
434
|
-
name=request.name,
|
|
435
|
-
rate_limit=request.rate_limit or 10,
|
|
436
|
-
expires_in_days=request.expires_in_days
|
|
437
|
-
)
|
|
438
|
-
|
|
439
|
-
return APIKeyCreateResponse(
|
|
440
|
-
api_key=api_key.key,
|
|
441
|
-
key_id=api_key.id,
|
|
442
|
-
user_id=user.id,
|
|
443
|
-
name=api_key.name,
|
|
444
|
-
created_at=api_key.created_at,
|
|
445
|
-
expires_at=api_key.expires_at,
|
|
446
|
-
rate_limit=api_key.rate_limit
|
|
447
|
-
)
|
|
448
|
-
except Exception as e:
|
|
449
|
-
logger.error(f"Error generating API key: {e}")
|
|
450
|
-
raise APIError(f"Failed to generate API key: {str(e)}", HTTP_500_INTERNAL_SERVER_ERROR)
|
|
451
|
-
|
|
452
|
-
@self.app.get(
|
|
453
|
-
"/v1/auth/validate",
|
|
454
|
-
response_model=APIKeyValidationResponse,
|
|
455
|
-
tags=["Authentication"],
|
|
456
|
-
description="Validate an API key and return its status."
|
|
457
|
-
)
|
|
458
|
-
async def validate_api_key(request: Request):
|
|
459
|
-
"""Validate an API key."""
|
|
460
|
-
if not api_key_manager:
|
|
461
|
-
raise APIError("Authentication system not initialized", HTTP_500_INTERNAL_SERVER_ERROR)
|
|
462
|
-
|
|
463
|
-
auth_header = request.headers.get("authorization")
|
|
464
|
-
if not auth_header:
|
|
465
|
-
return APIKeyValidationResponse(valid=False, error="No authorization header provided")
|
|
466
|
-
|
|
467
|
-
# Extract API key
|
|
468
|
-
api_key = auth_header
|
|
469
|
-
if auth_header.startswith("Bearer "):
|
|
470
|
-
api_key = auth_header[7:]
|
|
471
|
-
|
|
472
|
-
try:
|
|
473
|
-
is_valid, api_key_obj, error_msg = await api_key_manager.validate_api_key(api_key)
|
|
474
|
-
|
|
475
|
-
if is_valid and api_key_obj:
|
|
476
|
-
return APIKeyValidationResponse(
|
|
477
|
-
valid=True,
|
|
478
|
-
user_id=api_key_obj.user_id,
|
|
479
|
-
key_id=api_key_obj.id,
|
|
480
|
-
rate_limit=api_key_obj.rate_limit,
|
|
481
|
-
usage_count=api_key_obj.usage_count,
|
|
482
|
-
last_used_at=api_key_obj.last_used_at
|
|
483
|
-
)
|
|
484
|
-
else:
|
|
485
|
-
return APIKeyValidationResponse(valid=False, error=error_msg)
|
|
486
|
-
except Exception as e:
|
|
487
|
-
logger.error(f"Error validating API key: {e}")
|
|
488
|
-
return APIKeyValidationResponse(valid=False, error="Internal validation error")
|
|
489
|
-
|
|
490
|
-
@self.app.get(
|
|
491
|
-
"/health",
|
|
492
|
-
response_model=HealthCheckResponse,
|
|
493
|
-
tags=["Health"],
|
|
494
|
-
description="Health check endpoint for the API and database."
|
|
495
|
-
)
|
|
496
|
-
async def health_check():
|
|
497
|
-
"""Health check endpoint."""
|
|
498
|
-
db_status = "unknown"
|
|
499
|
-
db_manager = auth_components.get("db_manager")
|
|
500
|
-
if db_manager:
|
|
501
|
-
status_info = db_manager.get_status()
|
|
502
|
-
db_status = f"{status_info['type']} - {status_info['status']}"
|
|
503
|
-
|
|
504
|
-
return HealthCheckResponse(
|
|
505
|
-
status="healthy",
|
|
506
|
-
database=db_status,
|
|
507
|
-
timestamp=datetime.now(timezone.utc)
|
|
508
|
-
)
|
|
509
384
|
|
|
510
385
|
def _register_websearch_routes(self):
|
|
511
386
|
"""Register web search endpoint."""
|
|
@@ -583,18 +458,15 @@ class Api:
|
|
|
583
458
|
}
|
|
584
459
|
|
|
585
460
|
def _register_monitoring_routes(self):
|
|
586
|
-
"""Register monitoring and analytics routes
|
|
461
|
+
"""Register monitoring and analytics routes."""
|
|
587
462
|
|
|
588
463
|
@self.app.get(
|
|
589
464
|
"/monitor/requests",
|
|
590
465
|
tags=["Monitoring"],
|
|
591
|
-
description="Get recent API requests
|
|
466
|
+
description="Get recent API requests"
|
|
592
467
|
)
|
|
593
468
|
async def get_recent_requests(limit: int = Query(10, description="Number of recent requests to fetch")):
|
|
594
469
|
"""Get recent API requests for monitoring."""
|
|
595
|
-
if AppConfig.auth_required:
|
|
596
|
-
return {"error": "Monitoring is only available in no-auth mode"}
|
|
597
|
-
|
|
598
470
|
try:
|
|
599
471
|
return await request_logger.get_recent_requests(limit)
|
|
600
472
|
except Exception as e:
|
|
@@ -603,13 +475,10 @@ class Api:
|
|
|
603
475
|
@self.app.get(
|
|
604
476
|
"/monitor/stats",
|
|
605
477
|
tags=["Monitoring"],
|
|
606
|
-
description="Get API usage statistics
|
|
478
|
+
description="Get API usage statistics"
|
|
607
479
|
)
|
|
608
480
|
async def get_api_stats():
|
|
609
481
|
"""Get API usage statistics."""
|
|
610
|
-
if AppConfig.auth_required:
|
|
611
|
-
return {"error": "Monitoring is only available in no-auth mode"}
|
|
612
|
-
|
|
613
482
|
try:
|
|
614
483
|
return await request_logger.get_stats()
|
|
615
484
|
except Exception as e:
|
|
@@ -618,24 +487,13 @@ class Api:
|
|
|
618
487
|
@self.app.get(
|
|
619
488
|
"/monitor/health",
|
|
620
489
|
tags=["Monitoring"],
|
|
621
|
-
description="Health check
|
|
490
|
+
description="Health check"
|
|
622
491
|
)
|
|
623
492
|
async def enhanced_health_check():
|
|
624
|
-
"""Enhanced health check
|
|
493
|
+
"""Enhanced health check."""
|
|
625
494
|
try:
|
|
626
|
-
# Check database connectivity
|
|
627
|
-
db_status = "disconnected"
|
|
628
|
-
if request_logger.supabase_client:
|
|
629
|
-
try:
|
|
630
|
-
# Try a simple query to check connectivity
|
|
631
|
-
result = request_logger.supabase_client.table("api_requests").select("id").limit(1).execute()
|
|
632
|
-
db_status = "connected"
|
|
633
|
-
except Exception as e:
|
|
634
|
-
db_status = f"error: {str(e)[:100]}"
|
|
635
|
-
|
|
636
495
|
return {
|
|
637
496
|
"status": "healthy",
|
|
638
|
-
"database": db_status,
|
|
639
497
|
"auth_required": AppConfig.auth_required,
|
|
640
498
|
"rate_limit_enabled": AppConfig.rate_limit_enabled,
|
|
641
499
|
"request_logging_enabled": AppConfig.request_logging_enabled,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# webscout/server/schemas.py
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ErrorResponse(BaseModel):
|
|
9
|
+
"""Standard error response model."""
|
|
10
|
+
error: str = Field(..., description="Error message")
|
|
11
|
+
code: str = Field(..., description="Error code")
|
|
12
|
+
details: Optional[Dict[str, Any]] = Field(None, description="Additional error details")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HealthCheckResponse(BaseModel):
|
|
16
|
+
"""Health check response model."""
|
|
17
|
+
status: str = Field(..., description="Service status")
|
|
18
|
+
timestamp: datetime = Field(..., description="Check timestamp")
|
|
19
|
+
|
|
20
|
+
class Config:
|
|
21
|
+
json_encoders = {
|
|
22
|
+
datetime: lambda v: v.isoformat()
|
|
23
|
+
}
|
|
@@ -25,7 +25,7 @@ from webscout.Litlogger import Logger, LogLevel, LogFormat, ConsoleHandler
|
|
|
25
25
|
from .config import ServerConfig, AppConfig
|
|
26
26
|
from .routes import Api
|
|
27
27
|
from .providers import initialize_provider_map, initialize_tti_provider_map
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
|
|
30
30
|
# Configuration constants
|
|
31
31
|
DEFAULT_PORT = 8000
|
|
@@ -138,12 +138,7 @@ def create_app():
|
|
|
138
138
|
allow_headers=["*"],
|
|
139
139
|
)
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
initialize_auth_system(
|
|
143
|
-
app,
|
|
144
|
-
auth_required=AppConfig.auth_required,
|
|
145
|
-
rate_limit_enabled=AppConfig.rate_limit_enabled
|
|
146
|
-
)
|
|
141
|
+
|
|
147
142
|
# Add startup event handler
|
|
148
143
|
@app.on_event("startup")
|
|
149
144
|
async def startup():
|
|
@@ -175,42 +170,33 @@ def create_app_debug():
|
|
|
175
170
|
def start_server(
|
|
176
171
|
port: int = DEFAULT_PORT,
|
|
177
172
|
host: str = DEFAULT_HOST,
|
|
178
|
-
api_key: str = None,
|
|
179
173
|
default_provider: str = None,
|
|
180
174
|
base_url: str = None,
|
|
181
175
|
workers: int = 1,
|
|
182
176
|
log_level: str = 'info',
|
|
183
177
|
debug: bool = False,
|
|
184
|
-
no_auth: bool = False,
|
|
185
|
-
no_rate_limit: bool = False
|
|
186
178
|
):
|
|
187
179
|
"""Start the API server with the given configuration."""
|
|
188
180
|
run_api(
|
|
189
181
|
host=host,
|
|
190
182
|
port=port,
|
|
191
|
-
api_key=api_key,
|
|
192
183
|
default_provider=default_provider,
|
|
193
184
|
base_url=base_url,
|
|
194
185
|
workers=workers,
|
|
195
186
|
log_level=log_level,
|
|
196
187
|
debug=debug,
|
|
197
|
-
no_auth=no_auth,
|
|
198
|
-
no_rate_limit=no_rate_limit,
|
|
199
188
|
)
|
|
200
189
|
|
|
201
190
|
|
|
202
191
|
def run_api(
|
|
203
192
|
host: str = '0.0.0.0',
|
|
204
193
|
port: int = None,
|
|
205
|
-
api_key: str = None,
|
|
206
194
|
default_provider: str = None,
|
|
207
195
|
base_url: str = None,
|
|
208
196
|
debug: bool = False,
|
|
209
197
|
workers: int = 1,
|
|
210
198
|
log_level: str = 'info',
|
|
211
199
|
show_available_providers: bool = True,
|
|
212
|
-
no_auth: bool = False,
|
|
213
|
-
no_rate_limit: bool = False,
|
|
214
200
|
) -> None:
|
|
215
201
|
"""Run the API server with configuration."""
|
|
216
202
|
print("Starting Webscout OpenAI API server...")
|
|
@@ -218,11 +204,11 @@ def run_api(
|
|
|
218
204
|
port = DEFAULT_PORT
|
|
219
205
|
|
|
220
206
|
AppConfig.set_config(
|
|
221
|
-
api_key=
|
|
207
|
+
api_key=None,
|
|
222
208
|
default_provider=default_provider or AppConfig.default_provider,
|
|
223
209
|
base_url=base_url,
|
|
224
|
-
auth_required=
|
|
225
|
-
rate_limit_enabled=
|
|
210
|
+
auth_required=False,
|
|
211
|
+
rate_limit_enabled=False
|
|
226
212
|
)
|
|
227
213
|
|
|
228
214
|
if show_available_providers:
|
|
@@ -243,18 +229,10 @@ def run_api(
|
|
|
243
229
|
print(f"Docs URL: {api_endpoint_base}/docs")
|
|
244
230
|
|
|
245
231
|
# Show authentication status
|
|
246
|
-
|
|
247
|
-
print(f"Authentication: 🔓 DISABLED (No-Auth Mode)")
|
|
248
|
-
elif api_key:
|
|
249
|
-
print(f"Authentication: 🔐 Legacy API Key")
|
|
250
|
-
else:
|
|
251
|
-
print(f"Authentication: 🔑 Enhanced API Keys")
|
|
232
|
+
print(f"Authentication: 🔓 DISABLED")
|
|
252
233
|
|
|
253
234
|
# Show rate limiting status
|
|
254
|
-
|
|
255
|
-
print(f"Rate Limiting: ⚡ DISABLED")
|
|
256
|
-
else:
|
|
257
|
-
print(f"Rate Limiting: 🛡️ Enabled ({AppConfig.default_rate_limit}/min)")
|
|
235
|
+
print(f"Rate Limiting: ⚡ DISABLED")
|
|
258
236
|
|
|
259
237
|
print(f"Default Provider: {AppConfig.default_provider}")
|
|
260
238
|
print(f"Workers: {workers}")
|
|
@@ -291,7 +269,7 @@ def run_api(
|
|
|
291
269
|
print("\nUse Ctrl+C to stop the server.")
|
|
292
270
|
print("=" * 40 + "\n")
|
|
293
271
|
|
|
294
|
-
uvicorn_app_str = "webscout.
|
|
272
|
+
uvicorn_app_str = "webscout.server.server:create_app_debug" if debug else "webscout.server.server:create_app"
|
|
295
273
|
|
|
296
274
|
# Configure uvicorn settings
|
|
297
275
|
uvicorn_config = {
|
|
@@ -322,24 +300,18 @@ def main():
|
|
|
322
300
|
default_host = os.getenv('WEBSCOUT_HOST', DEFAULT_HOST)
|
|
323
301
|
default_workers = int(os.getenv('WEBSCOUT_WORKERS', '1'))
|
|
324
302
|
default_log_level = os.getenv('WEBSCOUT_LOG_LEVEL', 'info')
|
|
325
|
-
default_api_key = os.getenv('WEBSCOUT_API_KEY', os.getenv('API_KEY'))
|
|
326
303
|
default_provider = os.getenv('WEBSCOUT_DEFAULT_PROVIDER', os.getenv('DEFAULT_PROVIDER'))
|
|
327
304
|
default_base_url = os.getenv('WEBSCOUT_BASE_URL', os.getenv('BASE_URL'))
|
|
328
305
|
default_debug = os.getenv('WEBSCOUT_DEBUG', os.getenv('DEBUG', 'false')).lower() == 'true'
|
|
329
|
-
default_no_auth = os.getenv('WEBSCOUT_NO_AUTH', 'false').lower() == 'true'
|
|
330
|
-
default_no_rate_limit = os.getenv('WEBSCOUT_NO_RATE_LIMIT', 'false').lower() == 'true'
|
|
331
306
|
|
|
332
307
|
parser = argparse.ArgumentParser(description='Start Webscout OpenAI-compatible API server')
|
|
333
308
|
parser.add_argument('--port', type=int, default=default_port, help=f'Port to run the server on (default: {default_port})')
|
|
334
309
|
parser.add_argument('--host', type=str, default=default_host, help=f'Host to bind the server to (default: {default_host})')
|
|
335
310
|
parser.add_argument('--workers', type=int, default=default_workers, help=f'Number of worker processes (default: {default_workers})')
|
|
336
311
|
parser.add_argument('--log-level', type=str, default=default_log_level, choices=['debug', 'info', 'warning', 'error', 'critical'], help=f'Log level (default: {default_log_level})')
|
|
337
|
-
parser.add_argument('--api-key', type=str, default=default_api_key, help='API key for authentication (optional)')
|
|
338
312
|
parser.add_argument('--default-provider', type=str, default=default_provider, help='Default provider to use (optional)')
|
|
339
313
|
parser.add_argument('--base-url', type=str, default=default_base_url, help='Base URL for the API (optional, e.g., /api/v1)')
|
|
340
314
|
parser.add_argument('--debug', action='store_true', default=default_debug, help='Run in debug mode')
|
|
341
|
-
parser.add_argument('--no-auth', action='store_true', default=default_no_auth, help='Disable authentication (no API keys required)')
|
|
342
|
-
parser.add_argument('--no-rate-limit', action='store_true', default=default_no_rate_limit, help='Disable rate limiting (unlimited requests)')
|
|
343
315
|
args = parser.parse_args()
|
|
344
316
|
|
|
345
317
|
# Print configuration summary
|
|
@@ -349,9 +321,8 @@ def main():
|
|
|
349
321
|
print(f" Workers: {args.workers}")
|
|
350
322
|
print(f" Log Level: {args.log_level}")
|
|
351
323
|
print(f" Debug Mode: {args.debug}")
|
|
352
|
-
print(f"
|
|
353
|
-
print(f"
|
|
354
|
-
print(f" API Key: {'Set' if args.api_key else 'Not set'}")
|
|
324
|
+
print(f" Authentication: 🔓 DISABLED")
|
|
325
|
+
print(f" Rate Limiting: ⚡ DISABLED")
|
|
355
326
|
print(f" Default Provider: {args.default_provider or 'Not set'}")
|
|
356
327
|
print(f" Base URL: {args.base_url or 'Not set'}")
|
|
357
328
|
print()
|
|
@@ -361,12 +332,9 @@ def main():
|
|
|
361
332
|
port=args.port,
|
|
362
333
|
workers=args.workers,
|
|
363
334
|
log_level=args.log_level,
|
|
364
|
-
api_key=args.api_key,
|
|
365
335
|
default_provider=args.default_provider,
|
|
366
336
|
base_url=args.base_url,
|
|
367
|
-
debug=args.debug
|
|
368
|
-
no_auth=args.no_auth,
|
|
369
|
-
no_rate_limit=args.no_rate_limit
|
|
337
|
+
debug=args.debug
|
|
370
338
|
)
|
|
371
339
|
|
|
372
340
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple logger for no-auth mode.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
import sys
|
|
9
|
+
from webscout.Litlogger import Logger, LogLevel, LogFormat, ConsoleHandler
|
|
10
|
+
|
|
11
|
+
# Setup logger
|
|
12
|
+
logger = Logger(
|
|
13
|
+
name="webscout.api.simple_db",
|
|
14
|
+
level=LogLevel.INFO,
|
|
15
|
+
handlers=[ConsoleHandler(stream=sys.stdout)],
|
|
16
|
+
fmt=LogFormat.DEFAULT
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
class SimpleRequestLogger:
|
|
20
|
+
"""Simple request logger for no-auth mode."""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
logger.info("Simple request logger initialized (no database).")
|
|
24
|
+
|
|
25
|
+
async def log_request(
|
|
26
|
+
self,
|
|
27
|
+
request_id: str,
|
|
28
|
+
ip_address: str,
|
|
29
|
+
model: str,
|
|
30
|
+
question: str,
|
|
31
|
+
answer: str,
|
|
32
|
+
**kwargs
|
|
33
|
+
) -> bool:
|
|
34
|
+
"""Logs request details to the console."""
|
|
35
|
+
logger.info(f"Request {request_id}: model={model}, ip={ip_address}")
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
async def get_recent_requests(self, limit: int = 10) -> Dict[str, Any]:
|
|
39
|
+
"""Returns empty list of recent requests."""
|
|
40
|
+
logger.info("get_recent_requests called, but no database is configured.")
|
|
41
|
+
return {"requests": [], "count": 0}
|
|
42
|
+
|
|
43
|
+
async def get_stats(self) -> Dict[str, Any]:
|
|
44
|
+
"""Returns empty stats."""
|
|
45
|
+
logger.info("get_stats called, but no database is configured.")
|
|
46
|
+
return {"error": "No database configured.", "available": False}
|
|
47
|
+
|
|
48
|
+
# Global instance
|
|
49
|
+
request_logger = SimpleRequestLogger()
|
|
50
|
+
|
|
51
|
+
async def log_api_request(
|
|
52
|
+
request_id: str,
|
|
53
|
+
ip_address: str,
|
|
54
|
+
model: str,
|
|
55
|
+
question: str,
|
|
56
|
+
answer: str,
|
|
57
|
+
**kwargs
|
|
58
|
+
) -> bool:
|
|
59
|
+
"""Convenience function to log API requests."""
|
|
60
|
+
return await request_logger.log_request(
|
|
61
|
+
request_id=request_id,
|
|
62
|
+
ip_address=ip_address,
|
|
63
|
+
model=model,
|
|
64
|
+
question=question,
|
|
65
|
+
answer=answer,
|
|
66
|
+
**kwargs
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def get_client_ip(request) -> str:
|
|
70
|
+
"""Extract client IP address from request."""
|
|
71
|
+
forwarded_for = request.headers.get("X-Forwarded-For")
|
|
72
|
+
if forwarded_for:
|
|
73
|
+
return forwarded_for.split(",")[0].strip()
|
|
74
|
+
|
|
75
|
+
real_ip = request.headers.get("X-Real-IP")
|
|
76
|
+
if real_ip:
|
|
77
|
+
return real_ip.strip()
|
|
78
|
+
|
|
79
|
+
return getattr(request.client, "host", "unknown")
|
|
80
|
+
|
|
81
|
+
def generate_request_id() -> str:
|
|
82
|
+
"""Generate a unique request ID."""
|
|
83
|
+
import uuid
|
|
84
|
+
return str(uuid.uuid4())
|
webscout/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "2025.10.
|
|
1
|
+
__version__ = "2025.10.17"
|
|
2
2
|
__prog__ = "webscout"
|
webscout/version.py.bak
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = ""
|
|
1
|
+
__version__ = "2025.10.16"
|
|
2
2
|
__prog__ = "webscout"
|