webscout 8.3.1__py3-none-any.whl → 8.3.2__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 +46 -53
- webscout/Bing_search.py +418 -0
- webscout/Extra/gguf.py +706 -177
- webscout/Provider/AISEARCH/genspark_search.py +7 -7
- webscout/Provider/GeminiProxy.py +140 -0
- webscout/Provider/MCPCore.py +78 -75
- webscout/Provider/OPENAI/BLACKBOXAI.py +1 -4
- webscout/Provider/OPENAI/GeminiProxy.py +328 -0
- webscout/Provider/OPENAI/README.md +2 -0
- webscout/Provider/OPENAI/README_AUTOPROXY.md +238 -0
- webscout/Provider/OPENAI/__init__.py +15 -1
- webscout/Provider/OPENAI/autoproxy.py +332 -39
- webscout/Provider/OPENAI/base.py +15 -5
- webscout/Provider/OPENAI/e2b.py +0 -1
- webscout/Provider/OPENAI/mcpcore.py +109 -70
- webscout/Provider/OPENAI/scirachat.py +59 -51
- webscout/Provider/OPENAI/toolbaz.py +2 -9
- webscout/Provider/OPENAI/xenai.py +514 -0
- webscout/Provider/OPENAI/yep.py +8 -2
- webscout/Provider/TTI/__init__.py +1 -0
- webscout/Provider/TTI/bing.py +231 -0
- webscout/Provider/TTS/speechma.py +45 -39
- webscout/Provider/TogetherAI.py +366 -0
- webscout/Provider/XenAI.py +324 -0
- webscout/Provider/__init__.py +8 -3
- webscout/Provider/deepseek_assistant.py +378 -0
- webscout/auth/__init__.py +44 -0
- webscout/auth/api_key_manager.py +189 -0
- webscout/auth/auth_system.py +100 -0
- webscout/auth/config.py +76 -0
- webscout/auth/database.py +400 -0
- webscout/auth/exceptions.py +67 -0
- webscout/auth/middleware.py +248 -0
- webscout/auth/models.py +130 -0
- webscout/auth/providers.py +257 -0
- webscout/auth/rate_limiter.py +254 -0
- webscout/auth/request_models.py +127 -0
- webscout/auth/request_processing.py +226 -0
- webscout/auth/routes.py +526 -0
- webscout/auth/schemas.py +103 -0
- webscout/auth/server.py +312 -0
- webscout/auth/static/favicon.svg +11 -0
- webscout/auth/swagger_ui.py +203 -0
- webscout/auth/templates/components/authentication.html +237 -0
- webscout/auth/templates/components/base.html +103 -0
- webscout/auth/templates/components/endpoints.html +750 -0
- webscout/auth/templates/components/examples.html +491 -0
- webscout/auth/templates/components/footer.html +75 -0
- webscout/auth/templates/components/header.html +27 -0
- webscout/auth/templates/components/models.html +286 -0
- webscout/auth/templates/components/navigation.html +70 -0
- webscout/auth/templates/static/api.js +455 -0
- webscout/auth/templates/static/icons.js +168 -0
- webscout/auth/templates/static/main.js +784 -0
- webscout/auth/templates/static/particles.js +201 -0
- webscout/auth/templates/static/styles.css +3353 -0
- webscout/auth/templates/static/ui.js +374 -0
- webscout/auth/templates/swagger_ui.html +170 -0
- webscout/client.py +49 -3
- webscout/scout/core/scout.py +104 -26
- webscout/scout/element.py +139 -18
- webscout/swiftcli/core/cli.py +14 -3
- webscout/swiftcli/decorators/output.py +59 -9
- webscout/update_checker.py +31 -49
- webscout/version.py +1 -1
- webscout/webscout_search.py +4 -12
- webscout/webscout_search_async.py +3 -10
- webscout/yep_search.py +2 -11
- {webscout-8.3.1.dist-info → webscout-8.3.2.dist-info}/METADATA +41 -11
- {webscout-8.3.1.dist-info → webscout-8.3.2.dist-info}/RECORD +74 -36
- {webscout-8.3.1.dist-info → webscout-8.3.2.dist-info}/entry_points.txt +1 -1
- webscout/Provider/HF_space/__init__.py +0 -0
- webscout/Provider/HF_space/qwen_qwen2.py +0 -206
- webscout/Provider/OPENAI/api.py +0 -1320
- {webscout-8.3.1.dist-info → webscout-8.3.2.dist-info}/WHEEL +0 -0
- {webscout-8.3.1.dist-info → webscout-8.3.2.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.3.1.dist-info → webscout-8.3.2.dist-info}/top_level.txt +0 -0
webscout/auth/server.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Webscout OpenAI-Compatible API Server
|
|
3
|
+
|
|
4
|
+
A FastAPI-based server that provides OpenAI-compatible endpoints for various LLM providers.
|
|
5
|
+
Supports streaming and non-streaming chat completions with comprehensive error handling,
|
|
6
|
+
authentication, and provider management.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
import uvicorn
|
|
18
|
+
from fastapi import FastAPI
|
|
19
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
20
|
+
from fastapi.responses import RedirectResponse
|
|
21
|
+
|
|
22
|
+
from webscout.Litlogger import Logger, LogLevel, LogFormat, ConsoleHandler
|
|
23
|
+
from webscout.auth.swagger_ui import CustomSwaggerUI
|
|
24
|
+
from .config import ServerConfig, AppConfig
|
|
25
|
+
from .routes import Api
|
|
26
|
+
from .providers import initialize_provider_map, initialize_tti_provider_map
|
|
27
|
+
from .auth_system import initialize_auth_system
|
|
28
|
+
|
|
29
|
+
# Configuration constants
|
|
30
|
+
DEFAULT_PORT = 8000
|
|
31
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
32
|
+
API_VERSION = "v1"
|
|
33
|
+
|
|
34
|
+
# Setup Litlogger
|
|
35
|
+
logger = Logger(
|
|
36
|
+
name="webscout.api",
|
|
37
|
+
level=LogLevel.INFO,
|
|
38
|
+
handlers=[ConsoleHandler(stream=sys.stdout)],
|
|
39
|
+
fmt=LogFormat.DEFAULT
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Global configuration instance
|
|
43
|
+
config = ServerConfig()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_app():
|
|
47
|
+
"""Create and configure the FastAPI application."""
|
|
48
|
+
import os
|
|
49
|
+
app_title = os.getenv("WEBSCOUT_API_TITLE", "Webscout OpenAI API")
|
|
50
|
+
app_description = os.getenv("WEBSCOUT_API_DESCRIPTION", "OpenAI API compatible interface for various LLM providers with enhanced authentication")
|
|
51
|
+
app_version = os.getenv("WEBSCOUT_API_VERSION", "0.2.0")
|
|
52
|
+
app_docs_url = os.getenv("WEBSCOUT_API_DOCS_URL", "/docs")
|
|
53
|
+
app_redoc_url = os.getenv("WEBSCOUT_API_REDOC_URL", "/redoc")
|
|
54
|
+
app_openapi_url = os.getenv("WEBSCOUT_API_OPENAPI_URL", "/openapi.json")
|
|
55
|
+
|
|
56
|
+
app = FastAPI(
|
|
57
|
+
title=app_title,
|
|
58
|
+
description=app_description,
|
|
59
|
+
version=app_version,
|
|
60
|
+
docs_url=None, # Disable default docs
|
|
61
|
+
redoc_url=app_redoc_url,
|
|
62
|
+
openapi_url=app_openapi_url,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Add CORS middleware
|
|
66
|
+
app.add_middleware(
|
|
67
|
+
CORSMiddleware,
|
|
68
|
+
allow_origins=["*"],
|
|
69
|
+
allow_credentials=True,
|
|
70
|
+
allow_methods=["*"],
|
|
71
|
+
allow_headers=["*"],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Initialize authentication system with configuration
|
|
75
|
+
initialize_auth_system(
|
|
76
|
+
app,
|
|
77
|
+
auth_required=AppConfig.auth_required,
|
|
78
|
+
rate_limit_enabled=AppConfig.rate_limit_enabled
|
|
79
|
+
)
|
|
80
|
+
# Use custom Swagger UI (disable default docs)
|
|
81
|
+
app.docs_url = None
|
|
82
|
+
app.redoc_url = "/redoc" # keep ReDoc if desired
|
|
83
|
+
app.openapi_url = "/openapi.json"
|
|
84
|
+
CustomSwaggerUI(app)
|
|
85
|
+
# Add startup event handler
|
|
86
|
+
@app.on_event("startup")
|
|
87
|
+
async def startup():
|
|
88
|
+
if hasattr(app.state, 'startup_event'):
|
|
89
|
+
await app.state.startup_event()
|
|
90
|
+
|
|
91
|
+
# Initialize API routes
|
|
92
|
+
api = Api(app)
|
|
93
|
+
api.register_validation_exception_handler()
|
|
94
|
+
api.register_routes()
|
|
95
|
+
|
|
96
|
+
# Initialize providers
|
|
97
|
+
initialize_provider_map()
|
|
98
|
+
initialize_tti_provider_map()
|
|
99
|
+
|
|
100
|
+
# Root redirect
|
|
101
|
+
@app.get("/", include_in_schema=False)
|
|
102
|
+
async def root():
|
|
103
|
+
return RedirectResponse(url="/docs")
|
|
104
|
+
|
|
105
|
+
return app
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def create_app_debug():
|
|
109
|
+
"""Create app in debug mode."""
|
|
110
|
+
return create_app()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def start_server(
|
|
114
|
+
port: int = DEFAULT_PORT,
|
|
115
|
+
host: str = DEFAULT_HOST,
|
|
116
|
+
api_key: str = None,
|
|
117
|
+
default_provider: str = None,
|
|
118
|
+
base_url: str = None,
|
|
119
|
+
workers: int = 1,
|
|
120
|
+
log_level: str = 'info',
|
|
121
|
+
debug: bool = False,
|
|
122
|
+
no_auth: bool = False,
|
|
123
|
+
no_rate_limit: bool = False
|
|
124
|
+
):
|
|
125
|
+
"""Start the API server with the given configuration."""
|
|
126
|
+
run_api(
|
|
127
|
+
host=host,
|
|
128
|
+
port=port,
|
|
129
|
+
api_key=api_key,
|
|
130
|
+
default_provider=default_provider,
|
|
131
|
+
base_url=base_url,
|
|
132
|
+
workers=workers,
|
|
133
|
+
log_level=log_level,
|
|
134
|
+
debug=debug,
|
|
135
|
+
no_auth=no_auth,
|
|
136
|
+
no_rate_limit=no_rate_limit,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def run_api(
|
|
141
|
+
host: str = '0.0.0.0',
|
|
142
|
+
port: int = None,
|
|
143
|
+
api_key: str = None,
|
|
144
|
+
default_provider: str = None,
|
|
145
|
+
base_url: str = None,
|
|
146
|
+
debug: bool = False,
|
|
147
|
+
workers: int = 1,
|
|
148
|
+
log_level: str = 'info',
|
|
149
|
+
show_available_providers: bool = True,
|
|
150
|
+
no_auth: bool = False,
|
|
151
|
+
no_rate_limit: bool = False,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Run the API server with configuration."""
|
|
154
|
+
print("Starting Webscout OpenAI API server...")
|
|
155
|
+
if port is None:
|
|
156
|
+
port = DEFAULT_PORT
|
|
157
|
+
|
|
158
|
+
AppConfig.set_config(
|
|
159
|
+
api_key=api_key,
|
|
160
|
+
default_provider=default_provider or AppConfig.default_provider,
|
|
161
|
+
base_url=base_url,
|
|
162
|
+
auth_required=not no_auth,
|
|
163
|
+
rate_limit_enabled=not no_rate_limit
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if show_available_providers:
|
|
167
|
+
if not AppConfig.provider_map:
|
|
168
|
+
initialize_provider_map()
|
|
169
|
+
if not AppConfig.tti_provider_map:
|
|
170
|
+
initialize_tti_provider_map()
|
|
171
|
+
|
|
172
|
+
print("\n=== Webscout OpenAI API Server ===")
|
|
173
|
+
print(f"Server URL: http://{host if host != '0.0.0.0' else 'localhost'}:{port}")
|
|
174
|
+
if AppConfig.base_url:
|
|
175
|
+
print(f"Base Path: {AppConfig.base_url}")
|
|
176
|
+
api_endpoint_base = f"http://{host if host != '0.0.0.0' else 'localhost'}:{port}{AppConfig.base_url}"
|
|
177
|
+
else:
|
|
178
|
+
api_endpoint_base = f"http://{host if host != '0.0.0.0' else 'localhost'}:{port}"
|
|
179
|
+
|
|
180
|
+
print(f"API Endpoint: {api_endpoint_base}/v1/chat/completions")
|
|
181
|
+
print(f"Docs URL: {api_endpoint_base}/docs")
|
|
182
|
+
|
|
183
|
+
# Show authentication status
|
|
184
|
+
if no_auth:
|
|
185
|
+
print(f"Authentication: 🔓 DISABLED (No-Auth Mode)")
|
|
186
|
+
elif api_key:
|
|
187
|
+
print(f"Authentication: 🔐 Legacy API Key")
|
|
188
|
+
else:
|
|
189
|
+
print(f"Authentication: 🔑 Enhanced API Keys")
|
|
190
|
+
|
|
191
|
+
# Show rate limiting status
|
|
192
|
+
if no_rate_limit:
|
|
193
|
+
print(f"Rate Limiting: ⚡ DISABLED")
|
|
194
|
+
else:
|
|
195
|
+
print(f"Rate Limiting: 🛡️ Enabled ({AppConfig.default_rate_limit}/min)")
|
|
196
|
+
|
|
197
|
+
print(f"Default Provider: {AppConfig.default_provider}")
|
|
198
|
+
print(f"Workers: {workers}")
|
|
199
|
+
print(f"Log Level: {log_level}")
|
|
200
|
+
print(f"Debug Mode: {'Enabled' if debug else 'Disabled'}")
|
|
201
|
+
|
|
202
|
+
providers = list(set(v.__name__ for v in AppConfig.provider_map.values()))
|
|
203
|
+
print(f"\n--- Available Providers ({len(providers)}) ---")
|
|
204
|
+
for i, provider_name in enumerate(sorted(providers), 1):
|
|
205
|
+
print(f"{i}. {provider_name}")
|
|
206
|
+
|
|
207
|
+
provider_class_names = set(v.__name__ for v in AppConfig.provider_map.values())
|
|
208
|
+
models = sorted([model for model in AppConfig.provider_map.keys() if model not in provider_class_names])
|
|
209
|
+
if models:
|
|
210
|
+
print(f"\n--- Available Models ({len(models)}) ---")
|
|
211
|
+
for i, model_name in enumerate(models, 1):
|
|
212
|
+
print(f"{i}. {model_name} (via {AppConfig.provider_map[model_name].__name__})")
|
|
213
|
+
else:
|
|
214
|
+
print("\nNo specific models registered. Use provider names as models.")
|
|
215
|
+
|
|
216
|
+
tti_providers = list(set(v.__name__ for v in AppConfig.tti_provider_map.values()))
|
|
217
|
+
print(f"\n--- Available TTI Providers ({len(tti_providers)}) ---")
|
|
218
|
+
for i, provider_name in enumerate(sorted(tti_providers), 1):
|
|
219
|
+
print(f"{i}. {provider_name}")
|
|
220
|
+
|
|
221
|
+
tti_models = sorted([model for model in AppConfig.tti_provider_map.keys() if model not in tti_providers])
|
|
222
|
+
if tti_models:
|
|
223
|
+
print(f"\n--- Available TTI Models ({len(tti_models)}) ---")
|
|
224
|
+
for i, model_name in enumerate(tti_models, 1):
|
|
225
|
+
print(f"{i}. {model_name} (via {AppConfig.tti_provider_map[model_name].__name__})")
|
|
226
|
+
else:
|
|
227
|
+
print("\nNo specific TTI models registered. Use TTI provider names as models.")
|
|
228
|
+
|
|
229
|
+
print("\nUse Ctrl+C to stop the server.")
|
|
230
|
+
print("=" * 40 + "\n")
|
|
231
|
+
|
|
232
|
+
uvicorn_app_str = "webscout.auth.server:create_app_debug" if debug else "webscout.auth.server:create_app"
|
|
233
|
+
|
|
234
|
+
# Configure uvicorn settings
|
|
235
|
+
uvicorn_config = {
|
|
236
|
+
"app": uvicorn_app_str,
|
|
237
|
+
"host": host,
|
|
238
|
+
"port": int(port),
|
|
239
|
+
"factory": True,
|
|
240
|
+
"reload": debug,
|
|
241
|
+
"log_level": log_level.lower() if log_level else ("debug" if debug else "info"),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Add workers only if not in debug mode
|
|
245
|
+
if not debug and workers > 1:
|
|
246
|
+
uvicorn_config["workers"] = workers
|
|
247
|
+
print(f"Starting with {workers} workers...")
|
|
248
|
+
elif debug:
|
|
249
|
+
print("Debug mode enabled - using single worker with reload...")
|
|
250
|
+
|
|
251
|
+
uvicorn.run(**uvicorn_config)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def main():
|
|
255
|
+
"""Main entry point for the webscout-server console script."""
|
|
256
|
+
import argparse
|
|
257
|
+
|
|
258
|
+
# Read environment variables with fallbacks
|
|
259
|
+
default_port = int(os.getenv('WEBSCOUT_PORT', os.getenv('PORT', DEFAULT_PORT)))
|
|
260
|
+
default_host = os.getenv('WEBSCOUT_HOST', DEFAULT_HOST)
|
|
261
|
+
default_workers = int(os.getenv('WEBSCOUT_WORKERS', '1'))
|
|
262
|
+
default_log_level = os.getenv('WEBSCOUT_LOG_LEVEL', 'info')
|
|
263
|
+
default_api_key = os.getenv('WEBSCOUT_API_KEY', os.getenv('API_KEY'))
|
|
264
|
+
default_provider = os.getenv('WEBSCOUT_DEFAULT_PROVIDER', os.getenv('DEFAULT_PROVIDER'))
|
|
265
|
+
default_base_url = os.getenv('WEBSCOUT_BASE_URL', os.getenv('BASE_URL'))
|
|
266
|
+
default_debug = os.getenv('WEBSCOUT_DEBUG', os.getenv('DEBUG', 'false')).lower() == 'true'
|
|
267
|
+
default_no_auth = os.getenv('WEBSCOUT_NO_AUTH', 'false').lower() == 'true'
|
|
268
|
+
default_no_rate_limit = os.getenv('WEBSCOUT_NO_RATE_LIMIT', 'false').lower() == 'true'
|
|
269
|
+
|
|
270
|
+
parser = argparse.ArgumentParser(description='Start Webscout OpenAI-compatible API server')
|
|
271
|
+
parser.add_argument('--port', type=int, default=default_port, help=f'Port to run the server on (default: {default_port})')
|
|
272
|
+
parser.add_argument('--host', type=str, default=default_host, help=f'Host to bind the server to (default: {default_host})')
|
|
273
|
+
parser.add_argument('--workers', type=int, default=default_workers, help=f'Number of worker processes (default: {default_workers})')
|
|
274
|
+
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})')
|
|
275
|
+
parser.add_argument('--api-key', type=str, default=default_api_key, help='API key for authentication (optional)')
|
|
276
|
+
parser.add_argument('--default-provider', type=str, default=default_provider, help='Default provider to use (optional)')
|
|
277
|
+
parser.add_argument('--base-url', type=str, default=default_base_url, help='Base URL for the API (optional, e.g., /api/v1)')
|
|
278
|
+
parser.add_argument('--debug', action='store_true', default=default_debug, help='Run in debug mode')
|
|
279
|
+
parser.add_argument('--no-auth', action='store_true', default=default_no_auth, help='Disable authentication (no API keys required)')
|
|
280
|
+
parser.add_argument('--no-rate-limit', action='store_true', default=default_no_rate_limit, help='Disable rate limiting (unlimited requests)')
|
|
281
|
+
args = parser.parse_args()
|
|
282
|
+
|
|
283
|
+
# Print configuration summary
|
|
284
|
+
print(f"Configuration:")
|
|
285
|
+
print(f" Host: {args.host}")
|
|
286
|
+
print(f" Port: {args.port}")
|
|
287
|
+
print(f" Workers: {args.workers}")
|
|
288
|
+
print(f" Log Level: {args.log_level}")
|
|
289
|
+
print(f" Debug Mode: {args.debug}")
|
|
290
|
+
print(f" No-Auth Mode: {'🔓 ENABLED' if args.no_auth else '🔐 Disabled'}")
|
|
291
|
+
print(f" No Rate Limit: {'⚡ ENABLED' if args.no_rate_limit else '🛡️ Disabled'}")
|
|
292
|
+
print(f" API Key: {'Set' if args.api_key else 'Not set'}")
|
|
293
|
+
print(f" Default Provider: {args.default_provider or 'Not set'}")
|
|
294
|
+
print(f" Base URL: {args.base_url or 'Not set'}")
|
|
295
|
+
print()
|
|
296
|
+
|
|
297
|
+
run_api(
|
|
298
|
+
host=args.host,
|
|
299
|
+
port=args.port,
|
|
300
|
+
workers=args.workers,
|
|
301
|
+
log_level=args.log_level,
|
|
302
|
+
api_key=args.api_key,
|
|
303
|
+
default_provider=args.default_provider,
|
|
304
|
+
base_url=args.base_url,
|
|
305
|
+
debug=args.debug,
|
|
306
|
+
no_auth=args.no_auth,
|
|
307
|
+
no_rate_limit=args.no_rate_limit
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
main()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* Custom Swagger UI Favicon - SVG format */
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
3
|
+
<defs>
|
|
4
|
+
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
5
|
+
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
|
|
6
|
+
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1" />
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
<rect width="32" height="32" rx="8" fill="url(#grad1)"/>
|
|
10
|
+
<path d="M8 12h16M8 16h12M8 20h8" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Swagger UI implementation for Webscout FastAPI server.
|
|
3
|
+
Provides a modern, beautiful, and fully functional API documentation interface.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import logging
|
|
8
|
+
from fastapi import FastAPI, Request
|
|
9
|
+
from fastapi.responses import HTMLResponse
|
|
10
|
+
from fastapi.staticfiles import StaticFiles
|
|
11
|
+
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
|
|
12
|
+
|
|
13
|
+
class CustomSwaggerUI:
|
|
14
|
+
"""Custom Swagger UI handler for FastAPI applications."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, app: FastAPI):
|
|
17
|
+
self.app = app
|
|
18
|
+
self.template_dir = os.path.join(os.path.dirname(__file__), "templates")
|
|
19
|
+
self.static_dir = os.path.join(os.path.dirname(__file__), "static")
|
|
20
|
+
self.template_static_dir = os.path.join(self.template_dir, "static")
|
|
21
|
+
|
|
22
|
+
# Initialize Jinja2 environment
|
|
23
|
+
self.jinja_env = Environment(
|
|
24
|
+
loader=FileSystemLoader(self.template_dir),
|
|
25
|
+
autoescape=True
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Mount static files - prioritize template static files
|
|
29
|
+
static_mounted = False
|
|
30
|
+
|
|
31
|
+
# First try to mount template static files (higher priority)
|
|
32
|
+
if os.path.exists(self.template_static_dir):
|
|
33
|
+
try:
|
|
34
|
+
app.mount("/static", StaticFiles(directory=self.template_static_dir), name="template_static")
|
|
35
|
+
static_mounted = True
|
|
36
|
+
logging.info(f"Mounted template static files from: {self.template_static_dir}")
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logging.warning(f"Failed to mount template static files: {e}")
|
|
39
|
+
|
|
40
|
+
# Fallback to regular static directory if template static not available
|
|
41
|
+
if not static_mounted and os.path.exists(self.static_dir):
|
|
42
|
+
try:
|
|
43
|
+
app.mount("/static", StaticFiles(directory=self.static_dir), name="static")
|
|
44
|
+
logging.info(f"Mounted static files from: {self.static_dir}")
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logging.warning(f"Failed to mount static files: {e}")
|
|
47
|
+
|
|
48
|
+
# Log static file status
|
|
49
|
+
if not static_mounted:
|
|
50
|
+
logging.warning("No static files mounted - CSS and JS may not load correctly")
|
|
51
|
+
|
|
52
|
+
self._setup_routes()
|
|
53
|
+
|
|
54
|
+
def _setup_routes(self):
|
|
55
|
+
"""Setup custom Swagger UI routes."""
|
|
56
|
+
|
|
57
|
+
@self.app.get("/docs", response_class=HTMLResponse, include_in_schema=False)
|
|
58
|
+
async def custom_swagger_ui(request: Request):
|
|
59
|
+
"""Serve the custom Swagger UI."""
|
|
60
|
+
return await self._render_swagger_ui(request)
|
|
61
|
+
|
|
62
|
+
@self.app.get("/swagger-ui", response_class=HTMLResponse, include_in_schema=False)
|
|
63
|
+
async def custom_swagger_ui_alt(request: Request):
|
|
64
|
+
"""Alternative endpoint for custom Swagger UI."""
|
|
65
|
+
return await self._render_swagger_ui(request)
|
|
66
|
+
|
|
67
|
+
async def _render_swagger_ui(self, request: Request) -> HTMLResponse:
|
|
68
|
+
"""Render the custom Swagger UI template."""
|
|
69
|
+
try:
|
|
70
|
+
# Get app metadata
|
|
71
|
+
title = getattr(self.app, 'title', 'Webscout OpenAI API')
|
|
72
|
+
description = getattr(self.app, 'description', 'OpenAI API compatible interface')
|
|
73
|
+
version = getattr(self.app, 'version', '0.2.0')
|
|
74
|
+
|
|
75
|
+
# Get base URL
|
|
76
|
+
base_url = str(request.base_url).rstrip('/')
|
|
77
|
+
|
|
78
|
+
# Load and count models
|
|
79
|
+
model_count = await self._get_model_count()
|
|
80
|
+
provider_count = await self._get_provider_count()
|
|
81
|
+
|
|
82
|
+
# Load the main template using Jinja2 environment
|
|
83
|
+
template = self.jinja_env.get_template("swagger_ui.html")
|
|
84
|
+
|
|
85
|
+
# Render with context
|
|
86
|
+
rendered_html = template.render(
|
|
87
|
+
title=title,
|
|
88
|
+
description=description,
|
|
89
|
+
version=version,
|
|
90
|
+
base_url=base_url,
|
|
91
|
+
model_count=model_count,
|
|
92
|
+
provider_count=provider_count
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return HTMLResponse(content=rendered_html, status_code=200)
|
|
96
|
+
|
|
97
|
+
except TemplateNotFound:
|
|
98
|
+
# Template file doesn't exist, use fallback
|
|
99
|
+
logging.warning("Template file 'swagger_ui.html' not found, using fallback HTML")
|
|
100
|
+
return HTMLResponse(content=self._get_fallback_html(), status_code=200)
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
# Other errors, log and use fallback
|
|
104
|
+
logging.error(f"Error rendering Swagger UI template: {e}")
|
|
105
|
+
return HTMLResponse(content=self._get_fallback_html(), status_code=200)
|
|
106
|
+
|
|
107
|
+
async def _get_model_count(self) -> int:
|
|
108
|
+
"""Get the number of available models."""
|
|
109
|
+
try:
|
|
110
|
+
# Try to get from auth config
|
|
111
|
+
from .config import AppConfig
|
|
112
|
+
if hasattr(AppConfig, 'provider_map') and AppConfig.provider_map:
|
|
113
|
+
# Count models (keys with "/" are model names)
|
|
114
|
+
model_count = len([model for model in AppConfig.provider_map.keys() if "/" in model])
|
|
115
|
+
return model_count if model_count > 0 else 589
|
|
116
|
+
return 589 # Default fallback
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logging.debug(f"Could not get model count: {e}")
|
|
119
|
+
return 589
|
|
120
|
+
|
|
121
|
+
async def _get_provider_count(self) -> int:
|
|
122
|
+
"""Get the number of available providers."""
|
|
123
|
+
try:
|
|
124
|
+
# Try to get from auth config
|
|
125
|
+
from .config import AppConfig
|
|
126
|
+
if hasattr(AppConfig, 'provider_map') and AppConfig.provider_map:
|
|
127
|
+
# Count unique providers
|
|
128
|
+
providers = set()
|
|
129
|
+
for model_key in AppConfig.provider_map.keys():
|
|
130
|
+
if "/" in model_key:
|
|
131
|
+
provider_name = model_key.split("/")[0]
|
|
132
|
+
providers.add(provider_name)
|
|
133
|
+
return len(providers) if len(providers) > 0 else 42
|
|
134
|
+
return 42 # Default fallback
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logging.debug(f"Could not get provider count: {e}")
|
|
137
|
+
return 42
|
|
138
|
+
|
|
139
|
+
def _get_fallback_html(self) -> str:
|
|
140
|
+
"""Fallback HTML if template loading fails."""
|
|
141
|
+
return """
|
|
142
|
+
<!DOCTYPE html>
|
|
143
|
+
<html lang="en">
|
|
144
|
+
<head>
|
|
145
|
+
<meta charset="UTF-8">
|
|
146
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
147
|
+
<title>Webscout API Documentation</title>
|
|
148
|
+
<style>
|
|
149
|
+
body {
|
|
150
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
151
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
152
|
+
color: white;
|
|
153
|
+
margin: 0;
|
|
154
|
+
padding: 2rem;
|
|
155
|
+
min-height: 100vh;
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
justify-content: center;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
}
|
|
161
|
+
.container {
|
|
162
|
+
text-align: center;
|
|
163
|
+
background: rgba(255, 255, 255, 0.1);
|
|
164
|
+
backdrop-filter: blur(10px);
|
|
165
|
+
padding: 3rem;
|
|
166
|
+
border-radius: 1rem;
|
|
167
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
168
|
+
}
|
|
169
|
+
h1 { font-size: 3rem; margin-bottom: 1rem; }
|
|
170
|
+
p { font-size: 1.2rem; margin-bottom: 2rem; opacity: 0.9; }
|
|
171
|
+
.btn {
|
|
172
|
+
background: rgba(255, 255, 255, 0.2);
|
|
173
|
+
color: white;
|
|
174
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
175
|
+
padding: 1rem 2rem;
|
|
176
|
+
border-radius: 0.5rem;
|
|
177
|
+
text-decoration: none;
|
|
178
|
+
font-weight: 600;
|
|
179
|
+
transition: all 0.3s ease;
|
|
180
|
+
display: inline-block;
|
|
181
|
+
margin: 0.5rem;
|
|
182
|
+
}
|
|
183
|
+
.btn:hover {
|
|
184
|
+
background: rgba(255, 255, 255, 0.3);
|
|
185
|
+
transform: translateY(-2px);
|
|
186
|
+
}
|
|
187
|
+
</style>
|
|
188
|
+
</head>
|
|
189
|
+
<body>
|
|
190
|
+
<div class="container">
|
|
191
|
+
<h1>🚀 Webscout API</h1>
|
|
192
|
+
<p>OpenAI-Compatible API Documentation</p>
|
|
193
|
+
<a href="/redoc" class="btn">📖 ReDoc Documentation</a>
|
|
194
|
+
<a href="/openapi.json" class="btn">📋 OpenAPI Schema</a>
|
|
195
|
+
</div>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def setup_custom_swagger_ui(app: FastAPI) -> CustomSwaggerUI:
|
|
202
|
+
"""Setup custom Swagger UI for the FastAPI app."""
|
|
203
|
+
return CustomSwaggerUI(app)
|