webscout 8.2.9__py3-none-any.whl → 2026.1.19__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.
Files changed (413) hide show
  1. webscout/AIauto.py +524 -251
  2. webscout/AIbase.py +247 -319
  3. webscout/AIutel.py +68 -703
  4. webscout/Bard.py +1072 -1026
  5. webscout/Extra/GitToolkit/__init__.py +10 -10
  6. webscout/Extra/GitToolkit/gitapi/__init__.py +20 -12
  7. webscout/Extra/GitToolkit/gitapi/gist.py +142 -0
  8. webscout/Extra/GitToolkit/gitapi/organization.py +91 -0
  9. webscout/Extra/GitToolkit/gitapi/repository.py +308 -195
  10. webscout/Extra/GitToolkit/gitapi/search.py +162 -0
  11. webscout/Extra/GitToolkit/gitapi/trending.py +236 -0
  12. webscout/Extra/GitToolkit/gitapi/user.py +128 -96
  13. webscout/Extra/GitToolkit/gitapi/utils.py +82 -62
  14. webscout/Extra/YTToolkit/README.md +443 -375
  15. webscout/Extra/YTToolkit/YTdownloader.py +953 -957
  16. webscout/Extra/YTToolkit/__init__.py +3 -3
  17. webscout/Extra/YTToolkit/transcriber.py +595 -476
  18. webscout/Extra/YTToolkit/ytapi/README.md +230 -44
  19. webscout/Extra/YTToolkit/ytapi/__init__.py +22 -6
  20. webscout/Extra/YTToolkit/ytapi/captions.py +190 -0
  21. webscout/Extra/YTToolkit/ytapi/channel.py +302 -307
  22. webscout/Extra/YTToolkit/ytapi/errors.py +13 -13
  23. webscout/Extra/YTToolkit/ytapi/extras.py +178 -118
  24. webscout/Extra/YTToolkit/ytapi/hashtag.py +120 -0
  25. webscout/Extra/YTToolkit/ytapi/https.py +89 -88
  26. webscout/Extra/YTToolkit/ytapi/patterns.py +61 -61
  27. webscout/Extra/YTToolkit/ytapi/playlist.py +59 -59
  28. webscout/Extra/YTToolkit/ytapi/pool.py +8 -8
  29. webscout/Extra/YTToolkit/ytapi/query.py +143 -40
  30. webscout/Extra/YTToolkit/ytapi/shorts.py +122 -0
  31. webscout/Extra/YTToolkit/ytapi/stream.py +68 -63
  32. webscout/Extra/YTToolkit/ytapi/suggestions.py +97 -0
  33. webscout/Extra/YTToolkit/ytapi/utils.py +66 -62
  34. webscout/Extra/YTToolkit/ytapi/video.py +403 -232
  35. webscout/Extra/__init__.py +2 -3
  36. webscout/Extra/gguf.py +1298 -684
  37. webscout/Extra/tempmail/README.md +487 -487
  38. webscout/Extra/tempmail/__init__.py +28 -28
  39. webscout/Extra/tempmail/async_utils.py +143 -141
  40. webscout/Extra/tempmail/base.py +172 -161
  41. webscout/Extra/tempmail/cli.py +191 -187
  42. webscout/Extra/tempmail/emailnator.py +88 -84
  43. webscout/Extra/tempmail/mail_tm.py +378 -361
  44. webscout/Extra/tempmail/temp_mail_io.py +304 -292
  45. webscout/Extra/weather.py +196 -194
  46. webscout/Extra/weather_ascii.py +17 -15
  47. webscout/Provider/AISEARCH/PERPLEXED_search.py +175 -0
  48. webscout/Provider/AISEARCH/Perplexity.py +292 -333
  49. webscout/Provider/AISEARCH/README.md +106 -279
  50. webscout/Provider/AISEARCH/__init__.py +16 -9
  51. webscout/Provider/AISEARCH/brave_search.py +298 -0
  52. webscout/Provider/AISEARCH/iask_search.py +357 -410
  53. webscout/Provider/AISEARCH/monica_search.py +200 -220
  54. webscout/Provider/AISEARCH/webpilotai_search.py +242 -255
  55. webscout/Provider/Algion.py +413 -0
  56. webscout/Provider/Andi.py +74 -69
  57. webscout/Provider/Apriel.py +313 -0
  58. webscout/Provider/Ayle.py +323 -0
  59. webscout/Provider/ChatSandbox.py +329 -342
  60. webscout/Provider/ClaudeOnline.py +365 -0
  61. webscout/Provider/Cohere.py +232 -208
  62. webscout/Provider/DeepAI.py +367 -0
  63. webscout/Provider/Deepinfra.py +467 -340
  64. webscout/Provider/EssentialAI.py +217 -0
  65. webscout/Provider/ExaAI.py +274 -261
  66. webscout/Provider/Gemini.py +175 -169
  67. webscout/Provider/GithubChat.py +385 -369
  68. webscout/Provider/Gradient.py +286 -0
  69. webscout/Provider/Groq.py +556 -801
  70. webscout/Provider/HadadXYZ.py +323 -0
  71. webscout/Provider/HeckAI.py +392 -375
  72. webscout/Provider/HuggingFace.py +387 -0
  73. webscout/Provider/IBM.py +340 -0
  74. webscout/Provider/Jadve.py +317 -291
  75. webscout/Provider/K2Think.py +306 -0
  76. webscout/Provider/Koboldai.py +221 -384
  77. webscout/Provider/Netwrck.py +273 -270
  78. webscout/Provider/Nvidia.py +310 -0
  79. webscout/Provider/OPENAI/DeepAI.py +489 -0
  80. webscout/Provider/OPENAI/K2Think.py +423 -0
  81. webscout/Provider/OPENAI/PI.py +463 -0
  82. webscout/Provider/OPENAI/README.md +890 -952
  83. webscout/Provider/OPENAI/TogetherAI.py +405 -0
  84. webscout/Provider/OPENAI/TwoAI.py +255 -357
  85. webscout/Provider/OPENAI/__init__.py +148 -40
  86. webscout/Provider/OPENAI/ai4chat.py +348 -293
  87. webscout/Provider/OPENAI/akashgpt.py +436 -0
  88. webscout/Provider/OPENAI/algion.py +303 -0
  89. webscout/Provider/OPENAI/{exachat.py → ayle.py} +365 -444
  90. webscout/Provider/OPENAI/base.py +253 -249
  91. webscout/Provider/OPENAI/cerebras.py +296 -0
  92. webscout/Provider/OPENAI/chatgpt.py +870 -556
  93. webscout/Provider/OPENAI/chatsandbox.py +233 -173
  94. webscout/Provider/OPENAI/deepinfra.py +403 -322
  95. webscout/Provider/OPENAI/e2b.py +2370 -1414
  96. webscout/Provider/OPENAI/elmo.py +278 -0
  97. webscout/Provider/OPENAI/exaai.py +452 -417
  98. webscout/Provider/OPENAI/freeassist.py +446 -0
  99. webscout/Provider/OPENAI/gradient.py +448 -0
  100. webscout/Provider/OPENAI/groq.py +380 -364
  101. webscout/Provider/OPENAI/hadadxyz.py +292 -0
  102. webscout/Provider/OPENAI/heckai.py +333 -308
  103. webscout/Provider/OPENAI/huggingface.py +321 -0
  104. webscout/Provider/OPENAI/ibm.py +425 -0
  105. webscout/Provider/OPENAI/llmchat.py +253 -0
  106. webscout/Provider/OPENAI/llmchatco.py +378 -335
  107. webscout/Provider/OPENAI/meta.py +541 -0
  108. webscout/Provider/OPENAI/netwrck.py +374 -357
  109. webscout/Provider/OPENAI/nvidia.py +317 -0
  110. webscout/Provider/OPENAI/oivscode.py +348 -287
  111. webscout/Provider/OPENAI/openrouter.py +328 -0
  112. webscout/Provider/OPENAI/pydantic_imports.py +1 -172
  113. webscout/Provider/OPENAI/sambanova.py +397 -0
  114. webscout/Provider/OPENAI/sonus.py +305 -304
  115. webscout/Provider/OPENAI/textpollinations.py +370 -339
  116. webscout/Provider/OPENAI/toolbaz.py +375 -413
  117. webscout/Provider/OPENAI/typefully.py +419 -355
  118. webscout/Provider/OPENAI/typliai.py +279 -0
  119. webscout/Provider/OPENAI/utils.py +314 -318
  120. webscout/Provider/OPENAI/wisecat.py +359 -387
  121. webscout/Provider/OPENAI/writecream.py +185 -163
  122. webscout/Provider/OPENAI/x0gpt.py +462 -365
  123. webscout/Provider/OPENAI/zenmux.py +380 -0
  124. webscout/Provider/OpenRouter.py +386 -0
  125. webscout/Provider/Openai.py +337 -496
  126. webscout/Provider/PI.py +443 -429
  127. webscout/Provider/QwenLM.py +346 -254
  128. webscout/Provider/STT/__init__.py +28 -0
  129. webscout/Provider/STT/base.py +303 -0
  130. webscout/Provider/STT/elevenlabs.py +264 -0
  131. webscout/Provider/Sambanova.py +317 -0
  132. webscout/Provider/TTI/README.md +69 -82
  133. webscout/Provider/TTI/__init__.py +37 -7
  134. webscout/Provider/TTI/base.py +147 -64
  135. webscout/Provider/TTI/claudeonline.py +393 -0
  136. webscout/Provider/TTI/magicstudio.py +292 -201
  137. webscout/Provider/TTI/miragic.py +180 -0
  138. webscout/Provider/TTI/pollinations.py +331 -221
  139. webscout/Provider/TTI/together.py +334 -0
  140. webscout/Provider/TTI/utils.py +14 -11
  141. webscout/Provider/TTS/README.md +186 -192
  142. webscout/Provider/TTS/__init__.py +43 -10
  143. webscout/Provider/TTS/base.py +523 -159
  144. webscout/Provider/TTS/deepgram.py +286 -156
  145. webscout/Provider/TTS/elevenlabs.py +189 -111
  146. webscout/Provider/TTS/freetts.py +218 -0
  147. webscout/Provider/TTS/murfai.py +288 -113
  148. webscout/Provider/TTS/openai_fm.py +364 -129
  149. webscout/Provider/TTS/parler.py +203 -111
  150. webscout/Provider/TTS/qwen.py +334 -0
  151. webscout/Provider/TTS/sherpa.py +286 -0
  152. webscout/Provider/TTS/speechma.py +693 -580
  153. webscout/Provider/TTS/streamElements.py +275 -333
  154. webscout/Provider/TTS/utils.py +280 -280
  155. webscout/Provider/TextPollinationsAI.py +331 -308
  156. webscout/Provider/TogetherAI.py +450 -0
  157. webscout/Provider/TwoAI.py +309 -475
  158. webscout/Provider/TypliAI.py +311 -305
  159. webscout/Provider/UNFINISHED/ChatHub.py +219 -209
  160. webscout/Provider/{OPENAI/glider.py → UNFINISHED/ChutesAI.py} +331 -326
  161. webscout/Provider/{GizAI.py → UNFINISHED/GizAI.py} +300 -295
  162. webscout/Provider/{Marcus.py → UNFINISHED/Marcus.py} +218 -198
  163. webscout/Provider/UNFINISHED/Qodo.py +481 -0
  164. webscout/Provider/{MCPCore.py → UNFINISHED/XenAI.py} +330 -315
  165. webscout/Provider/UNFINISHED/Youchat.py +347 -330
  166. webscout/Provider/UNFINISHED/aihumanizer.py +41 -0
  167. webscout/Provider/UNFINISHED/grammerchecker.py +37 -0
  168. webscout/Provider/UNFINISHED/liner.py +342 -0
  169. webscout/Provider/UNFINISHED/liner_api_request.py +246 -263
  170. webscout/Provider/{samurai.py → UNFINISHED/samurai.py} +231 -224
  171. webscout/Provider/WiseCat.py +256 -233
  172. webscout/Provider/WrDoChat.py +390 -370
  173. webscout/Provider/__init__.py +115 -174
  174. webscout/Provider/ai4chat.py +181 -174
  175. webscout/Provider/akashgpt.py +330 -335
  176. webscout/Provider/cerebras.py +397 -290
  177. webscout/Provider/cleeai.py +236 -213
  178. webscout/Provider/elmo.py +291 -283
  179. webscout/Provider/geminiapi.py +343 -208
  180. webscout/Provider/julius.py +245 -223
  181. webscout/Provider/learnfastai.py +333 -325
  182. webscout/Provider/llama3mitril.py +230 -215
  183. webscout/Provider/llmchat.py +308 -258
  184. webscout/Provider/llmchatco.py +321 -306
  185. webscout/Provider/meta.py +996 -801
  186. webscout/Provider/oivscode.py +332 -309
  187. webscout/Provider/searchchat.py +316 -292
  188. webscout/Provider/sonus.py +264 -258
  189. webscout/Provider/toolbaz.py +359 -353
  190. webscout/Provider/turboseek.py +332 -266
  191. webscout/Provider/typefully.py +262 -202
  192. webscout/Provider/x0gpt.py +332 -299
  193. webscout/__init__.py +31 -39
  194. webscout/__main__.py +5 -5
  195. webscout/cli.py +585 -524
  196. webscout/client.py +1497 -70
  197. webscout/conversation.py +140 -436
  198. webscout/exceptions.py +383 -362
  199. webscout/litagent/__init__.py +29 -29
  200. webscout/litagent/agent.py +492 -455
  201. webscout/litagent/constants.py +60 -60
  202. webscout/models.py +505 -181
  203. webscout/optimizers.py +74 -420
  204. webscout/prompt_manager.py +376 -288
  205. webscout/sanitize.py +1514 -0
  206. webscout/scout/README.md +452 -404
  207. webscout/scout/__init__.py +8 -8
  208. webscout/scout/core/__init__.py +7 -7
  209. webscout/scout/core/crawler.py +330 -210
  210. webscout/scout/core/scout.py +800 -607
  211. webscout/scout/core/search_result.py +51 -96
  212. webscout/scout/core/text_analyzer.py +64 -63
  213. webscout/scout/core/text_utils.py +412 -277
  214. webscout/scout/core/web_analyzer.py +54 -52
  215. webscout/scout/element.py +872 -478
  216. webscout/scout/parsers/__init__.py +70 -69
  217. webscout/scout/parsers/html5lib_parser.py +182 -172
  218. webscout/scout/parsers/html_parser.py +238 -236
  219. webscout/scout/parsers/lxml_parser.py +203 -178
  220. webscout/scout/utils.py +38 -37
  221. webscout/search/__init__.py +47 -0
  222. webscout/search/base.py +201 -0
  223. webscout/search/bing_main.py +45 -0
  224. webscout/search/brave_main.py +92 -0
  225. webscout/search/duckduckgo_main.py +57 -0
  226. webscout/search/engines/__init__.py +127 -0
  227. webscout/search/engines/bing/__init__.py +15 -0
  228. webscout/search/engines/bing/base.py +35 -0
  229. webscout/search/engines/bing/images.py +114 -0
  230. webscout/search/engines/bing/news.py +96 -0
  231. webscout/search/engines/bing/suggestions.py +36 -0
  232. webscout/search/engines/bing/text.py +109 -0
  233. webscout/search/engines/brave/__init__.py +19 -0
  234. webscout/search/engines/brave/base.py +47 -0
  235. webscout/search/engines/brave/images.py +213 -0
  236. webscout/search/engines/brave/news.py +353 -0
  237. webscout/search/engines/brave/suggestions.py +318 -0
  238. webscout/search/engines/brave/text.py +167 -0
  239. webscout/search/engines/brave/videos.py +364 -0
  240. webscout/search/engines/duckduckgo/__init__.py +25 -0
  241. webscout/search/engines/duckduckgo/answers.py +80 -0
  242. webscout/search/engines/duckduckgo/base.py +189 -0
  243. webscout/search/engines/duckduckgo/images.py +100 -0
  244. webscout/search/engines/duckduckgo/maps.py +183 -0
  245. webscout/search/engines/duckduckgo/news.py +70 -0
  246. webscout/search/engines/duckduckgo/suggestions.py +22 -0
  247. webscout/search/engines/duckduckgo/text.py +221 -0
  248. webscout/search/engines/duckduckgo/translate.py +48 -0
  249. webscout/search/engines/duckduckgo/videos.py +80 -0
  250. webscout/search/engines/duckduckgo/weather.py +84 -0
  251. webscout/search/engines/mojeek.py +61 -0
  252. webscout/search/engines/wikipedia.py +77 -0
  253. webscout/search/engines/yahoo/__init__.py +41 -0
  254. webscout/search/engines/yahoo/answers.py +19 -0
  255. webscout/search/engines/yahoo/base.py +34 -0
  256. webscout/search/engines/yahoo/images.py +323 -0
  257. webscout/search/engines/yahoo/maps.py +19 -0
  258. webscout/search/engines/yahoo/news.py +258 -0
  259. webscout/search/engines/yahoo/suggestions.py +140 -0
  260. webscout/search/engines/yahoo/text.py +273 -0
  261. webscout/search/engines/yahoo/translate.py +19 -0
  262. webscout/search/engines/yahoo/videos.py +302 -0
  263. webscout/search/engines/yahoo/weather.py +220 -0
  264. webscout/search/engines/yandex.py +67 -0
  265. webscout/search/engines/yep/__init__.py +13 -0
  266. webscout/search/engines/yep/base.py +34 -0
  267. webscout/search/engines/yep/images.py +101 -0
  268. webscout/search/engines/yep/suggestions.py +38 -0
  269. webscout/search/engines/yep/text.py +99 -0
  270. webscout/search/http_client.py +172 -0
  271. webscout/search/results.py +141 -0
  272. webscout/search/yahoo_main.py +57 -0
  273. webscout/search/yep_main.py +48 -0
  274. webscout/server/__init__.py +48 -0
  275. webscout/server/config.py +78 -0
  276. webscout/server/exceptions.py +69 -0
  277. webscout/server/providers.py +286 -0
  278. webscout/server/request_models.py +131 -0
  279. webscout/server/request_processing.py +404 -0
  280. webscout/server/routes.py +642 -0
  281. webscout/server/server.py +351 -0
  282. webscout/server/ui_templates.py +1171 -0
  283. webscout/swiftcli/__init__.py +79 -95
  284. webscout/swiftcli/core/__init__.py +7 -7
  285. webscout/swiftcli/core/cli.py +574 -297
  286. webscout/swiftcli/core/context.py +98 -104
  287. webscout/swiftcli/core/group.py +268 -241
  288. webscout/swiftcli/decorators/__init__.py +28 -28
  289. webscout/swiftcli/decorators/command.py +243 -221
  290. webscout/swiftcli/decorators/options.py +247 -220
  291. webscout/swiftcli/decorators/output.py +392 -252
  292. webscout/swiftcli/exceptions.py +21 -21
  293. webscout/swiftcli/plugins/__init__.py +9 -9
  294. webscout/swiftcli/plugins/base.py +134 -135
  295. webscout/swiftcli/plugins/manager.py +269 -269
  296. webscout/swiftcli/utils/__init__.py +58 -59
  297. webscout/swiftcli/utils/formatting.py +251 -252
  298. webscout/swiftcli/utils/parsing.py +368 -267
  299. webscout/update_checker.py +280 -136
  300. webscout/utils.py +28 -14
  301. webscout/version.py +2 -1
  302. webscout/version.py.bak +3 -0
  303. webscout/zeroart/__init__.py +218 -135
  304. webscout/zeroart/base.py +70 -66
  305. webscout/zeroart/effects.py +155 -101
  306. webscout/zeroart/fonts.py +1799 -1239
  307. webscout-2026.1.19.dist-info/METADATA +638 -0
  308. webscout-2026.1.19.dist-info/RECORD +312 -0
  309. {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/WHEEL +1 -1
  310. {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/entry_points.txt +1 -1
  311. webscout/DWEBS.py +0 -520
  312. webscout/Extra/Act.md +0 -309
  313. webscout/Extra/GitToolkit/gitapi/README.md +0 -110
  314. webscout/Extra/autocoder/__init__.py +0 -9
  315. webscout/Extra/autocoder/autocoder.py +0 -1105
  316. webscout/Extra/autocoder/autocoder_utiles.py +0 -332
  317. webscout/Extra/gguf.md +0 -430
  318. webscout/Extra/weather.md +0 -281
  319. webscout/Litlogger/README.md +0 -10
  320. webscout/Litlogger/__init__.py +0 -15
  321. webscout/Litlogger/formats.py +0 -4
  322. webscout/Litlogger/handlers.py +0 -103
  323. webscout/Litlogger/levels.py +0 -13
  324. webscout/Litlogger/logger.py +0 -92
  325. webscout/Provider/AI21.py +0 -177
  326. webscout/Provider/AISEARCH/DeepFind.py +0 -254
  327. webscout/Provider/AISEARCH/felo_search.py +0 -202
  328. webscout/Provider/AISEARCH/genspark_search.py +0 -324
  329. webscout/Provider/AISEARCH/hika_search.py +0 -186
  330. webscout/Provider/AISEARCH/scira_search.py +0 -298
  331. webscout/Provider/Aitopia.py +0 -316
  332. webscout/Provider/AllenAI.py +0 -440
  333. webscout/Provider/Blackboxai.py +0 -791
  334. webscout/Provider/ChatGPTClone.py +0 -237
  335. webscout/Provider/ChatGPTGratis.py +0 -194
  336. webscout/Provider/Cloudflare.py +0 -324
  337. webscout/Provider/ExaChat.py +0 -358
  338. webscout/Provider/Flowith.py +0 -217
  339. webscout/Provider/FreeGemini.py +0 -250
  340. webscout/Provider/Glider.py +0 -225
  341. webscout/Provider/HF_space/__init__.py +0 -0
  342. webscout/Provider/HF_space/qwen_qwen2.py +0 -206
  343. webscout/Provider/HuggingFaceChat.py +0 -469
  344. webscout/Provider/Hunyuan.py +0 -283
  345. webscout/Provider/LambdaChat.py +0 -411
  346. webscout/Provider/Llama3.py +0 -259
  347. webscout/Provider/Nemotron.py +0 -218
  348. webscout/Provider/OLLAMA.py +0 -396
  349. webscout/Provider/OPENAI/BLACKBOXAI.py +0 -766
  350. webscout/Provider/OPENAI/Cloudflare.py +0 -378
  351. webscout/Provider/OPENAI/FreeGemini.py +0 -283
  352. webscout/Provider/OPENAI/NEMOTRON.py +0 -232
  353. webscout/Provider/OPENAI/Qwen3.py +0 -283
  354. webscout/Provider/OPENAI/api.py +0 -969
  355. webscout/Provider/OPENAI/c4ai.py +0 -373
  356. webscout/Provider/OPENAI/chatgptclone.py +0 -494
  357. webscout/Provider/OPENAI/copilot.py +0 -242
  358. webscout/Provider/OPENAI/flowith.py +0 -162
  359. webscout/Provider/OPENAI/freeaichat.py +0 -359
  360. webscout/Provider/OPENAI/mcpcore.py +0 -389
  361. webscout/Provider/OPENAI/multichat.py +0 -376
  362. webscout/Provider/OPENAI/opkfc.py +0 -496
  363. webscout/Provider/OPENAI/scirachat.py +0 -477
  364. webscout/Provider/OPENAI/standardinput.py +0 -433
  365. webscout/Provider/OPENAI/typegpt.py +0 -364
  366. webscout/Provider/OPENAI/uncovrAI.py +0 -463
  367. webscout/Provider/OPENAI/venice.py +0 -431
  368. webscout/Provider/OPENAI/yep.py +0 -382
  369. webscout/Provider/OpenGPT.py +0 -209
  370. webscout/Provider/Perplexitylabs.py +0 -415
  371. webscout/Provider/Reka.py +0 -214
  372. webscout/Provider/StandardInput.py +0 -290
  373. webscout/Provider/TTI/aiarta.py +0 -365
  374. webscout/Provider/TTI/artbit.py +0 -0
  375. webscout/Provider/TTI/fastflux.py +0 -200
  376. webscout/Provider/TTI/piclumen.py +0 -203
  377. webscout/Provider/TTI/pixelmuse.py +0 -225
  378. webscout/Provider/TTS/gesserit.py +0 -128
  379. webscout/Provider/TTS/sthir.py +0 -94
  380. webscout/Provider/TeachAnything.py +0 -229
  381. webscout/Provider/UNFINISHED/puterjs.py +0 -635
  382. webscout/Provider/UNFINISHED/test_lmarena.py +0 -119
  383. webscout/Provider/Venice.py +0 -258
  384. webscout/Provider/VercelAI.py +0 -253
  385. webscout/Provider/Writecream.py +0 -246
  386. webscout/Provider/WritingMate.py +0 -269
  387. webscout/Provider/asksteve.py +0 -220
  388. webscout/Provider/chatglm.py +0 -215
  389. webscout/Provider/copilot.py +0 -425
  390. webscout/Provider/freeaichat.py +0 -285
  391. webscout/Provider/granite.py +0 -235
  392. webscout/Provider/hermes.py +0 -266
  393. webscout/Provider/koala.py +0 -170
  394. webscout/Provider/lmarena.py +0 -198
  395. webscout/Provider/multichat.py +0 -364
  396. webscout/Provider/scira_chat.py +0 -299
  397. webscout/Provider/scnet.py +0 -243
  398. webscout/Provider/talkai.py +0 -194
  399. webscout/Provider/typegpt.py +0 -289
  400. webscout/Provider/uncovr.py +0 -368
  401. webscout/Provider/yep.py +0 -389
  402. webscout/litagent/Readme.md +0 -276
  403. webscout/litprinter/__init__.py +0 -59
  404. webscout/swiftcli/Readme.md +0 -323
  405. webscout/tempid.py +0 -128
  406. webscout/webscout_search.py +0 -1184
  407. webscout/webscout_search_async.py +0 -654
  408. webscout/yep_search.py +0 -347
  409. webscout/zeroart/README.md +0 -89
  410. webscout-8.2.9.dist-info/METADATA +0 -1033
  411. webscout-8.2.9.dist-info/RECORD +0 -289
  412. {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/licenses/LICENSE.md +0 -0
  413. {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/top_level.txt +0 -0
@@ -1,969 +0,0 @@
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 json
12
- import logging
13
- import os
14
- import secrets
15
- import sys
16
- import time
17
- import uuid
18
- import inspect
19
- from typing import List, Dict, Optional, Union, Any, Generator, Callable
20
- import types
21
-
22
- import uvicorn
23
- from fastapi import FastAPI, Response, Request, Body
24
- from fastapi.middleware.cors import CORSMiddleware
25
- from fastapi.responses import StreamingResponse, RedirectResponse, JSONResponse
26
- from fastapi.openapi.utils import get_openapi
27
- from fastapi.routing import APIRoute
28
- from fastapi.exceptions import RequestValidationError
29
- from fastapi.security import APIKeyHeader
30
- from starlette.exceptions import HTTPException as StarletteHTTPException
31
- from starlette.status import (
32
- HTTP_422_UNPROCESSABLE_ENTITY,
33
- HTTP_404_NOT_FOUND,
34
- HTTP_401_UNAUTHORIZED,
35
- HTTP_403_FORBIDDEN,
36
- HTTP_500_INTERNAL_SERVER_ERROR,
37
- )
38
-
39
- from webscout.Provider.OPENAI.pydantic_imports import BaseModel, Field
40
- from typing import Literal
41
-
42
- # Import provider classes from the OPENAI directory
43
- from webscout.Provider.OPENAI import *
44
- from webscout.Provider.OPENAI.utils import (
45
- ChatCompletion, Choice, ChatCompletionMessage, CompletionUsage
46
- )
47
-
48
-
49
- # Configuration constants
50
- DEFAULT_PORT = 8000
51
- DEFAULT_HOST = "0.0.0.0"
52
- API_VERSION = "v1"
53
-
54
- # Setup logging
55
- logging.basicConfig(
56
- level=logging.INFO,
57
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
58
- handlers=[
59
- logging.StreamHandler(sys.stdout),
60
- ]
61
- )
62
- logger = logging.getLogger("webscout.api")
63
-
64
-
65
- class ServerConfig:
66
- """Centralized configuration management for the API server."""
67
-
68
- def __init__(self):
69
- self.api_key: Optional[str] = None
70
- self.provider_map: Dict[str, Any] = {}
71
- self.default_provider: str = "ChatGPT"
72
- self.base_url: Optional[str] = None
73
- self.host: str = DEFAULT_HOST
74
- self.port: int = DEFAULT_PORT
75
- self.debug: bool = False
76
- self.cors_origins: List[str] = ["*"]
77
- self.max_request_size: int = 10 * 1024 * 1024 # 10MB
78
- self.request_timeout: int = 300 # 5 minutes
79
-
80
- def update(self, **kwargs) -> None:
81
- """Update configuration with provided values."""
82
- for key, value in kwargs.items():
83
- if hasattr(self, key) and value is not None:
84
- setattr(self, key, value)
85
- logger.info(f"Config updated: {key} = {value}")
86
-
87
- def validate(self) -> None:
88
- """Validate configuration settings."""
89
- if self.port < 1 or self.port > 65535:
90
- raise ValueError(f"Invalid port number: {self.port}")
91
-
92
- if self.default_provider not in self.provider_map and self.provider_map:
93
- available_providers = list(set(v.__name__ for v in self.provider_map.values()))
94
- logger.warning(f"Default provider '{self.default_provider}' not found. Available: {available_providers}")
95
-
96
-
97
- # Global configuration instance
98
- config = ServerConfig()
99
-
100
-
101
- # Define Pydantic models for multimodal content parts, aligning with OpenAI's API
102
- class TextPart(BaseModel):
103
- """Text content part for multimodal messages."""
104
- type: Literal["text"]
105
- text: str
106
-
107
-
108
- class ImageURL(BaseModel):
109
- """Image URL configuration for multimodal messages."""
110
- url: str # Can be http(s) or data URI
111
- detail: Optional[Literal["auto", "low", "high"]] = Field(
112
- "auto",
113
- description="Specifies the detail level of the image."
114
- )
115
-
116
-
117
- class ImagePart(BaseModel):
118
- """Image content part for multimodal messages."""
119
- type: Literal["image_url"]
120
- image_url: ImageURL
121
-
122
-
123
- MessageContentParts = Union[TextPart, ImagePart]
124
-
125
-
126
- class Message(BaseModel):
127
- """Chat message model compatible with OpenAI API."""
128
- role: Literal["system", "user", "assistant", "function", "tool"]
129
- content: Optional[Union[str, List[MessageContentParts]]] = Field(
130
- None,
131
- description="The content of the message. Can be a string, a list of content parts (for multimodal), or null."
132
- )
133
- name: Optional[str] = None
134
- # Future: Add tool_calls and tool_call_id for function calling support
135
- # tool_calls: Optional[List[ToolCall]] = None
136
- # tool_call_id: Optional[str] = None
137
-
138
- class ChatCompletionRequest(BaseModel):
139
- model: str = Field(..., description="ID of the model to use. See the model endpoint for the available models.")
140
- messages: List[Message] = Field(..., description="A list of messages comprising the conversation so far.")
141
- temperature: Optional[float] = Field(None, description="What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.")
142
- top_p: Optional[float] = Field(None, description="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.")
143
- n: Optional[int] = Field(1, description="How many chat completion choices to generate for each input message.")
144
- stream: Optional[bool] = Field(False, description="If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message.")
145
- max_tokens: Optional[int] = Field(None, description="The maximum number of tokens to generate in the chat completion.")
146
- presence_penalty: Optional[float] = Field(None, description="Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.")
147
- frequency_penalty: Optional[float] = Field(None, description="Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.")
148
- logit_bias: Optional[Dict[str, float]] = Field(None, description="Modify the likelihood of specified tokens appearing in the completion.")
149
- user: Optional[str] = Field(None, description="A unique identifier representing your end-user, which can help the API to monitor and detect abuse.")
150
- stop: Optional[Union[str, List[str]]] = Field(None, description="Up to 4 sequences where the API will stop generating further tokens.")
151
-
152
- class Config:
153
- extra = "ignore" # Ignore extra fields that aren't in the model
154
- schema_extra = {
155
- "example": {
156
- "model": "ChatGPT/gpt-4",
157
- "messages": [
158
- {"role": "system", "content": "You are a helpful assistant."},
159
- {"role": "user", "content": "Hello, how are you?"}
160
- ],
161
- "temperature": 0.7,
162
- "max_tokens": 150,
163
- "stream": False
164
- }
165
- }
166
-
167
- class ModelInfo(BaseModel):
168
- """Model information for the models endpoint."""
169
- id: str
170
- object: str = "model"
171
- created: int
172
- owned_by: str
173
-
174
-
175
- class ModelListResponse(BaseModel):
176
- """Response model for the models list endpoint."""
177
- object: str = "list"
178
- data: List[ModelInfo]
179
-
180
-
181
- class ErrorDetail(BaseModel):
182
- """Error detail structure compatible with OpenAI API."""
183
- message: str
184
- type: str = "server_error"
185
- param: Optional[str] = None
186
- code: Optional[str] = None
187
-
188
-
189
- class ErrorResponse(BaseModel):
190
- """Error response structure compatible with OpenAI API."""
191
- error: ErrorDetail
192
-
193
-
194
- class APIError(Exception):
195
- """Custom exception for API errors."""
196
-
197
- def __init__(self, message: str, status_code: int = HTTP_500_INTERNAL_SERVER_ERROR,
198
- error_type: str = "server_error", param: Optional[str] = None,
199
- code: Optional[str] = None):
200
- self.message = message
201
- self.status_code = status_code
202
- self.error_type = error_type
203
- self.param = param
204
- self.code = code
205
- super().__init__(message)
206
-
207
- def to_response(self) -> JSONResponse:
208
- """Convert to FastAPI JSONResponse."""
209
- error_detail = ErrorDetail(
210
- message=self.message,
211
- type=self.error_type,
212
- param=self.param,
213
- code=self.code
214
- )
215
- error_response = ErrorResponse(error=error_detail)
216
- return JSONResponse(
217
- status_code=self.status_code,
218
- content=error_response.model_dump(exclude_none=True)
219
- )
220
-
221
-
222
- class AppConfig:
223
- """Legacy configuration class for backward compatibility."""
224
- api_key: Optional[str] = None
225
- provider_map = {}
226
- default_provider = "ChatGPT"
227
- base_url: Optional[str] = None
228
-
229
- @classmethod
230
- def set_config(cls, **data):
231
- """Set configuration values."""
232
- for key, value in data.items():
233
- setattr(cls, key, value)
234
- # Sync with new config system
235
- config.update(**data)
236
-
237
- # Custom route class to handle dynamic base URLs
238
- # Note: The /docs 404 issue is likely related to server execution (Werkzeug logs vs. Uvicorn script).
239
- # This DynamicBaseRoute, when AppConfig.base_url is None, should act as a passthrough and not break /docs.
240
- # If AppConfig.base_url is set, this route class has limitations in correctly handling prefixed routes
241
- # without more complex path manipulation or using FastAPI's APIRouter prefixing/mounting features.
242
- class DynamicBaseRoute(APIRoute):
243
- def get_route_handler(self) -> Callable:
244
- original_route_handler = super().get_route_handler()
245
- async def custom_route_handler(request: Request) -> Response:
246
- if AppConfig.base_url:
247
- if not request.url.path.startswith(AppConfig.base_url):
248
- # This logic might need refinement if base_url is used.
249
- # For API routes not matching the prefix, a 404 might be appropriate.
250
- # Docs routes (/docs, /openapi.json) are usually at the root.
251
- # The current 'pass' allows root docs even if base_url is set for APIs.
252
- pass
253
- return await original_route_handler(request)
254
- return custom_route_handler
255
-
256
- def create_app():
257
- app = FastAPI(
258
- title="Webscout OpenAI API",
259
- description="OpenAI API compatible interface for various LLM providers",
260
- version="0.1.0",
261
- docs_url="/docs",
262
- redoc_url="/redoc",
263
- openapi_url="/openapi.json",
264
- )
265
- app.router.route_class = DynamicBaseRoute
266
- app.add_middleware(
267
- CORSMiddleware,
268
- allow_origins=["*"],
269
- allow_credentials=True,
270
- allow_methods=["*"],
271
- allow_headers=["*"],
272
- )
273
- api = Api(app)
274
- api.register_authorization()
275
- api.register_validation_exception_handler()
276
- api.register_routes()
277
- initialize_provider_map()
278
-
279
- def custom_openapi():
280
- if app.openapi_schema:
281
- return app.openapi_schema
282
-
283
- openapi_schema = get_openapi(
284
- title=app.title,
285
- version=app.version,
286
- description=app.description,
287
- routes=app.routes,
288
- )
289
-
290
- if "components" not in openapi_schema: openapi_schema["components"] = {}
291
- if "schemas" not in openapi_schema["components"]: openapi_schema["components"]["schemas"] = {}
292
-
293
- # Use Pydantic's schema generation for accuracy
294
- # Assuming Pydantic v1 .schema() or v2 .model_json_schema() based on pydantic_imports
295
- # For broader compatibility, trying .schema() first.
296
- # If using Pydantic v2 primarily, .model_json_schema() is preferred.
297
- schema_method_name = "model_json_schema" if hasattr(BaseModel, "model_json_schema") else "schema"
298
-
299
- # Add/update schemas derived from Pydantic models to ensure they are correctly defined
300
- pydantic_models_to_register = {
301
- "TextPart": TextPart,
302
- "ImageURL": ImageURL,
303
- "ImagePart": ImagePart,
304
- "Message": Message,
305
- "ChatCompletionRequest": ChatCompletionRequest,
306
- }
307
-
308
- for name, model_cls in pydantic_models_to_register.items():
309
- if schema_method_name == "model_json_schema":
310
- schema_data = model_cls.model_json_schema(ref_template="#/components/schemas/{model}")
311
- else:
312
- schema_data = model_cls.schema()
313
- # Pydantic might add a "title" to the schema, which is often not desired for component schemas
314
- if "title" in schema_data:
315
- del schema_data["title"]
316
- openapi_schema["components"]["schemas"][name] = schema_data
317
-
318
- app.openapi_schema = openapi_schema
319
- return app.openapi_schema
320
-
321
- app.openapi = custom_openapi
322
- return app
323
-
324
- def create_app_debug():
325
- return create_app()
326
-
327
- def initialize_provider_map() -> None:
328
- """Initialize the provider map by discovering available providers."""
329
- logger.info("Initializing provider map...")
330
-
331
- try:
332
- from webscout.Provider.OPENAI.base import OpenAICompatibleProvider
333
- module = sys.modules["webscout.Provider.OPENAI"]
334
-
335
- provider_count = 0
336
- model_count = 0
337
-
338
- for name, obj in inspect.getmembers(module):
339
- if (
340
- inspect.isclass(obj)
341
- and issubclass(obj, OpenAICompatibleProvider)
342
- and obj.__name__ != "OpenAICompatibleProvider"
343
- ):
344
- provider_name = obj.__name__
345
- AppConfig.provider_map[provider_name] = obj
346
- config.provider_map[provider_name] = obj
347
- provider_count += 1
348
-
349
- # Register available models for this provider
350
- if hasattr(obj, "AVAILABLE_MODELS") and isinstance(
351
- obj.AVAILABLE_MODELS, (list, tuple, set)
352
- ):
353
- for model in obj.AVAILABLE_MODELS:
354
- if model and isinstance(model, str):
355
- model_key = f"{provider_name}/{model}"
356
- AppConfig.provider_map[model_key] = obj
357
- config.provider_map[model_key] = obj
358
- model_count += 1
359
-
360
- # Fallback to ChatGPT if no providers found
361
- if not AppConfig.provider_map:
362
- logger.warning("No providers found, using ChatGPT fallback")
363
- try:
364
- from webscout.Provider.OPENAI.chatgpt import ChatGPT
365
- fallback_models = ["gpt-4", "gpt-4o", "gpt-4o-mini", "gpt-3.5-turbo"]
366
-
367
- AppConfig.provider_map["ChatGPT"] = ChatGPT
368
- config.provider_map["ChatGPT"] = ChatGPT
369
-
370
- for model in fallback_models:
371
- model_key = f"ChatGPT/{model}"
372
- AppConfig.provider_map[model_key] = ChatGPT
373
- config.provider_map[model_key] = ChatGPT
374
-
375
- AppConfig.default_provider = "ChatGPT"
376
- config.default_provider = "ChatGPT"
377
- provider_count = 1
378
- model_count = len(fallback_models)
379
- except ImportError as e:
380
- logger.error(f"Failed to import ChatGPT fallback: {e}")
381
- raise APIError("No providers available", HTTP_500_INTERNAL_SERVER_ERROR)
382
-
383
- logger.info(f"Initialized {provider_count} providers with {model_count} models")
384
-
385
- except Exception as e:
386
- logger.error(f"Failed to initialize provider map: {e}")
387
- raise APIError(f"Provider initialization failed: {e}", HTTP_500_INTERNAL_SERVER_ERROR)
388
-
389
- class Api:
390
- def __init__(self, app: FastAPI) -> None:
391
- self.app = app
392
- self.get_api_key = APIKeyHeader(name="authorization", auto_error=False)
393
-
394
- def register_authorization(self):
395
- @self.app.middleware("http")
396
- async def authorization(request: Request, call_next):
397
- if AppConfig.api_key is not None:
398
- auth_header = await self.get_api_key(request)
399
- path = request.url.path
400
- if path.startswith("/v1"): # Only protect /v1 routes
401
- # Also allow access to /docs, /openapi.json etc. if AppConfig.base_url is not set or path is not under it
402
- # This logic should be fine as it only protects /v1 paths
403
- if auth_header is None:
404
- return ErrorResponse.from_message("API key required", HTTP_401_UNAUTHORIZED)
405
- if auth_header.startswith("Bearer "):
406
- auth_header = auth_header[7:]
407
- if AppConfig.api_key is None or not secrets.compare_digest(AppConfig.api_key, auth_header): # AppConfig.api_key check is redundant after outer if
408
- return ErrorResponse.from_message("Invalid API key", HTTP_403_FORBIDDEN)
409
- return await call_next(request)
410
-
411
- def register_validation_exception_handler(self):
412
- """Register comprehensive exception handlers."""
413
-
414
- @self.app.exception_handler(APIError)
415
- async def api_error_handler(request: Request, exc: APIError):
416
- """Handle custom API errors."""
417
- logger.error(f"API Error: {exc.message} (Status: {exc.status_code})")
418
- return exc.to_response()
419
-
420
- @self.app.exception_handler(RequestValidationError)
421
- async def validation_exception_handler(request: Request, exc: RequestValidationError):
422
- errors = exc.errors()
423
- error_messages = []
424
- body = await request.body()
425
- is_empty_body = not body or body.strip() in (b"", b"null", b"{}")
426
- for error in errors:
427
- loc = error.get("loc", [])
428
- # Ensure loc_str is user-friendly
429
- loc_str_parts = []
430
- for item in loc:
431
- if item == "body": # Skip "body" part if it's the first element of a longer path
432
- if len(loc) > 1: continue
433
- loc_str_parts.append(str(item))
434
- loc_str = " -> ".join(loc_str_parts)
435
-
436
- msg = error.get("msg", "Validation error")
437
-
438
- # Check if this error is for the 'content' field specifically due to multimodal input
439
- if len(loc) >=3 and loc[0] == 'body' and loc[1] == 'messages' and loc[-1] == 'content':
440
- # Check if the error type suggests a string was expected but a list (or vice-versa) was given for content
441
- if "Input should be a valid string" in msg and error.get("input_type") == "list":
442
- error_messages.append({
443
- "loc": loc,
444
- "message": f"Invalid message content: {msg}. Ensure content matches the expected format (string or list of content parts). Path: {loc_str}",
445
- "type": error.get("type", "validation_error")
446
- })
447
- continue # Skip default message formatting for this specific case
448
- elif "Input should be a valid list" in msg and error.get("input_type") == "string":
449
- error_messages.append({
450
- "loc": loc,
451
- "message": f"Invalid message content: {msg}. Ensure content matches the expected format (string or list of content parts). Path: {loc_str}",
452
- "type": error.get("type", "validation_error")
453
- })
454
- continue
455
-
456
- if "body" in loc:
457
- if len(loc) > 1 and loc[1] == "messages":
458
- error_messages.append({
459
- "loc": loc,
460
- "message": "The 'messages' field is required and must be a non-empty array of message objects. " + f"Error: {msg} at {loc_str}",
461
- "type": error.get("type", "validation_error")
462
- })
463
- elif len(loc) > 1 and loc[1] == "model":
464
- error_messages.append({
465
- "loc": loc,
466
- "message": "The 'model' field is required and must be a string. " + f"Error: {msg} at {loc_str}",
467
- "type": error.get("type", "validation_error")
468
- })
469
- else:
470
- error_messages.append({
471
- "loc": loc,
472
- "message": f"{msg} at {loc_str}",
473
- "type": error.get("type", "validation_error")
474
- })
475
- else:
476
- error_messages.append({
477
- "loc": loc,
478
- "message": f"{msg} at {loc_str}",
479
- "type": error.get("type", "validation_error")
480
- })
481
- if request.url.path == "/v1/chat/completions":
482
- example = ChatCompletionRequest.Config.schema_extra["example"]
483
- if is_empty_body:
484
- return JSONResponse(
485
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
486
- content={
487
- "error": {
488
- "message": "Request body is required and must include 'model' and 'messages'.",
489
- "type": "invalid_request_error",
490
- "param": None,
491
- "code": "body_missing"
492
- },
493
- "example": example
494
- }
495
- )
496
- return JSONResponse(
497
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
498
- content={"detail": error_messages, "example": example}
499
- )
500
- return JSONResponse(
501
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
502
- content={"detail": error_messages}
503
- )
504
- @self.app.exception_handler(StarletteHTTPException)
505
- async def http_exception_handler(request: Request, exc: StarletteHTTPException):
506
- return JSONResponse(
507
- status_code=exc.status_code,
508
- content={"detail": exc.detail}
509
- )
510
- @self.app.exception_handler(Exception)
511
- async def general_exception_handler(request: Request, exc: Exception):
512
- return JSONResponse(
513
- status_code=HTTP_500_INTERNAL_SERVER_ERROR,
514
- content={"detail": f"Internal server error: {str(exc)}"}
515
- )
516
-
517
- def register_routes(self):
518
- @self.app.get("/", include_in_schema=False)
519
- async def root():
520
- # Note: If /docs is 404ing, check if server is Uvicorn (expected) or Werkzeug (from logs).
521
- # Werkzeug logs suggest possible execution of a Flask app or WSGI misconfiguration.
522
- # This api.py file is intended for Uvicorn.
523
- return RedirectResponse(url="/docs")
524
-
525
- @self.app.get("/v1/models", response_model=ModelListResponse)
526
- async def list_models():
527
- models = []
528
- for model_name, provider_class in AppConfig.provider_map.items():
529
- if "/" not in model_name:
530
- continue # Skip provider names
531
- if any(m["id"] == model_name for m in models):
532
- continue
533
- models.append({
534
- "id": model_name,
535
- "object": "model",
536
- "created": int(time.time()),
537
- "owned_by": provider_class.__name__
538
- })
539
- return {
540
- "object": "list",
541
- "data": models
542
- }
543
-
544
- @self.app.post(
545
- "/v1/chat/completions",
546
- response_model_exclude_none=True,
547
- response_model_exclude_unset=True,
548
- openapi_extra={ # This ensures the example is shown in docs
549
- "requestBody": {
550
- "content": {
551
- "application/json": {
552
- "schema": {
553
- "$ref": "#/components/schemas/ChatCompletionRequest" # Relies on custom_openapi
554
- },
555
- "example": ChatCompletionRequest.Config.schema_extra["example"]
556
- }
557
- }
558
- }
559
- }
560
- )
561
- async def chat_completions(
562
- chat_request: ChatCompletionRequest = Body(...)
563
- ):
564
- """Handle chat completion requests with comprehensive error handling."""
565
- start_time = time.time()
566
- request_id = f"chatcmpl-{uuid.uuid4()}"
567
-
568
- try:
569
- logger.info(f"Processing chat completion request {request_id} for model: {chat_request.model}")
570
-
571
- # Resolve provider and model
572
- provider_class, model_name = resolve_provider_and_model(chat_request.model)
573
-
574
- # Initialize provider with error handling
575
- try:
576
- provider = provider_class()
577
- logger.debug(f"Initialized provider: {provider_class.__name__}")
578
- except Exception as e:
579
- logger.error(f"Failed to initialize provider {provider_class.__name__}: {e}")
580
- raise APIError(
581
- f"Failed to initialize provider {provider_class.__name__}: {e}",
582
- HTTP_500_INTERNAL_SERVER_ERROR,
583
- "provider_error"
584
- )
585
-
586
- # Process and validate messages
587
- processed_messages = process_messages(chat_request.messages)
588
-
589
- # Prepare parameters for provider
590
- params = prepare_provider_params(chat_request, model_name, processed_messages)
591
-
592
- # Handle streaming vs non-streaming
593
- if chat_request.stream:
594
- return await handle_streaming_response(provider, params, request_id)
595
- else:
596
- return await handle_non_streaming_response(provider, params, request_id, start_time)
597
-
598
- except APIError:
599
- # Re-raise API errors as-is
600
- raise
601
- except Exception as e:
602
- logger.error(f"Unexpected error in chat completion {request_id}: {e}")
603
- raise APIError(
604
- f"Internal server error: {str(e)}",
605
- HTTP_500_INTERNAL_SERVER_ERROR,
606
- "internal_error"
607
- )
608
-
609
-
610
- def resolve_provider_and_model(model_identifier: str) -> tuple[Any, str]:
611
- """Resolve provider class and model name from model identifier."""
612
- provider_class = None
613
- model_name = None
614
-
615
- # Check for explicit provider/model syntax
616
- if model_identifier in AppConfig.provider_map and "/" in model_identifier:
617
- provider_class = AppConfig.provider_map[model_identifier]
618
- _, model_name = model_identifier.split("/", 1)
619
- elif "/" in model_identifier:
620
- provider_name, model_name = model_identifier.split("/", 1)
621
- provider_class = AppConfig.provider_map.get(provider_name)
622
- else:
623
- provider_class = AppConfig.provider_map.get(AppConfig.default_provider)
624
- model_name = model_identifier
625
-
626
- if not provider_class:
627
- available_providers = list(set(v.__name__ for v in AppConfig.provider_map.values()))
628
- raise APIError(
629
- f"Provider for model '{model_identifier}' not found. Available providers: {available_providers}",
630
- HTTP_404_NOT_FOUND,
631
- "model_not_found",
632
- param="model"
633
- )
634
-
635
- # Validate model availability
636
- if hasattr(provider_class, "AVAILABLE_MODELS") and model_name is not None:
637
- available = getattr(provider_class, "AVAILABLE_MODELS", [])
638
- if available and model_name not in available:
639
- raise APIError(
640
- f"Model '{model_name}' not supported by provider '{provider_class.__name__}'. Available models: {available}",
641
- HTTP_404_NOT_FOUND,
642
- "model_not_found",
643
- param="model"
644
- )
645
-
646
- return provider_class, model_name
647
-
648
-
649
- def process_messages(messages: List[Message]) -> List[Dict[str, Any]]:
650
- """Process and validate chat messages."""
651
- processed_messages = []
652
-
653
- for i, msg_in in enumerate(messages):
654
- try:
655
- message_dict_out = {"role": msg_in.role}
656
-
657
- if msg_in.content is None:
658
- message_dict_out["content"] = None
659
- elif isinstance(msg_in.content, str):
660
- message_dict_out["content"] = msg_in.content
661
- else: # List[MessageContentParts]
662
- message_dict_out["content"] = [
663
- part.model_dump(exclude_none=True) for part in msg_in.content
664
- ]
665
-
666
- if msg_in.name:
667
- message_dict_out["name"] = msg_in.name
668
-
669
- processed_messages.append(message_dict_out)
670
-
671
- except Exception as e:
672
- raise APIError(
673
- f"Invalid message at index {i}: {str(e)}",
674
- HTTP_422_UNPROCESSABLE_ENTITY,
675
- "invalid_request_error",
676
- param=f"messages[{i}]"
677
- )
678
-
679
- return processed_messages
680
-
681
-
682
- def prepare_provider_params(chat_request: ChatCompletionRequest, model_name: str,
683
- processed_messages: List[Dict[str, Any]]) -> Dict[str, Any]:
684
- """Prepare parameters for the provider."""
685
- params = {
686
- "model": model_name,
687
- "messages": processed_messages,
688
- "stream": chat_request.stream,
689
- }
690
-
691
- # Add optional parameters if present
692
- optional_params = [
693
- "temperature", "max_tokens", "top_p", "presence_penalty",
694
- "frequency_penalty", "stop", "user"
695
- ]
696
-
697
- for param in optional_params:
698
- value = getattr(chat_request, param, None)
699
- if value is not None:
700
- params[param] = value
701
-
702
- return params
703
-
704
-
705
- async def handle_streaming_response(provider: Any, params: Dict[str, Any], request_id: str) -> StreamingResponse:
706
- """Handle streaming chat completion response."""
707
- async def streaming():
708
- try:
709
- logger.debug(f"Starting streaming response for request {request_id}")
710
- completion_stream = provider.chat.completions.create(**params)
711
-
712
- # Check if it's iterable (generator, iterator, or other iterable types)
713
- if hasattr(completion_stream, '__iter__') and not isinstance(completion_stream, (str, bytes, dict)):
714
- try:
715
- for chunk in completion_stream:
716
- # Standardize chunk format before sending
717
- if hasattr(chunk, 'model_dump'): # Pydantic v2
718
- chunk_data = chunk.model_dump(exclude_none=True)
719
- elif hasattr(chunk, 'dict'): # Pydantic v1
720
- chunk_data = chunk.dict(exclude_none=True)
721
- elif isinstance(chunk, dict):
722
- chunk_data = chunk
723
- else: # Fallback for unknown chunk types
724
- chunk_data = chunk
725
- yield f"data: {json.dumps(chunk_data)}\n\n"
726
- except TypeError as te:
727
- logger.error(f"Error iterating over completion_stream: {te}")
728
- # Fall back to treating as non-generator response
729
- if hasattr(completion_stream, 'model_dump'):
730
- yield f"data: {json.dumps(completion_stream.model_dump(exclude_none=True))}\n\n"
731
- elif hasattr(completion_stream, 'dict'):
732
- yield f"data: {json.dumps(completion_stream.dict(exclude_none=True))}\n\n"
733
- else:
734
- yield f"data: {json.dumps(completion_stream)}\n\n"
735
- else: # Non-generator response
736
- if hasattr(completion_stream, 'model_dump'):
737
- yield f"data: {json.dumps(completion_stream.model_dump(exclude_none=True))}\n\n"
738
- elif hasattr(completion_stream, 'dict'):
739
- yield f"data: {json.dumps(completion_stream.dict(exclude_none=True))}\n\n"
740
- else:
741
- yield f"data: {json.dumps(completion_stream)}\n\n"
742
-
743
- except Exception as e:
744
- logger.error(f"Error in streaming response for request {request_id}: {e}")
745
- error_data = {
746
- "error": {
747
- "message": str(e),
748
- "type": "server_error",
749
- "code": "streaming_error"
750
- }
751
- }
752
- yield f"data: {json.dumps(error_data)}\n\n"
753
- finally:
754
- yield "data: [DONE]\n\n"
755
- return StreamingResponse(streaming(), media_type="text/event-stream")
756
-
757
-
758
- async def handle_non_streaming_response(provider: Any, params: Dict[str, Any],
759
- request_id: str, start_time: float) -> Dict[str, Any]:
760
- """Handle non-streaming chat completion response."""
761
- try:
762
- logger.debug(f"Starting non-streaming response for request {request_id}")
763
- completion = provider.chat.completions.create(**params)
764
-
765
- if completion is None:
766
- # Return a valid OpenAI-compatible error response
767
- return ChatCompletion(
768
- id=request_id,
769
- created=int(time.time()),
770
- model=params.get("model", "unknown"),
771
- choices=[Choice(
772
- index=0,
773
- message=ChatCompletionMessage(role="assistant", content="No response generated."),
774
- finish_reason="error"
775
- )],
776
- usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0)
777
- ).model_dump(exclude_none=True)
778
-
779
- # Standardize response format
780
- if hasattr(completion, "model_dump"): # Pydantic v2
781
- response_data = completion.model_dump(exclude_none=True)
782
- elif hasattr(completion, "dict"): # Pydantic v1
783
- response_data = completion.dict(exclude_none=True)
784
- elif isinstance(completion, dict):
785
- response_data = completion
786
- else:
787
- raise APIError(
788
- "Invalid response format from provider",
789
- HTTP_500_INTERNAL_SERVER_ERROR,
790
- "provider_error"
791
- )
792
-
793
- elapsed = time.time() - start_time
794
- logger.info(f"Completed non-streaming request {request_id} in {elapsed:.2f}s")
795
-
796
- return response_data
797
-
798
- except Exception as e:
799
- logger.error(f"Error in non-streaming response for request {request_id}: {e}")
800
- raise APIError(
801
- f"Provider error: {str(e)}",
802
- HTTP_500_INTERNAL_SERVER_ERROR,
803
- "provider_error"
804
- )
805
-
806
- def format_exception(e: Union[Exception, str]) -> str:
807
- if isinstance(e, str):
808
- message = e
809
- else:
810
- message = f"{e.__class__.__name__}: {str(e)}" # Keep it concise
811
- return json.dumps({
812
- "error": {
813
- "message": message,
814
- "type": "server_error", # Or more specific if possible
815
- "param": None,
816
- "code": "internal_server_error" # Or more specific
817
- }
818
- })
819
-
820
- def start_server(
821
- port: int = DEFAULT_PORT,
822
- host: str = DEFAULT_HOST,
823
- api_key: str = None,
824
- default_provider: str = None,
825
- base_url: str = None,
826
- workers: int = 1,
827
- log_level: str = 'info',
828
- debug: bool = False
829
- ):
830
- """Start the API server with the given configuration."""
831
- run_api(
832
- host=host,
833
- port=port,
834
- api_key=api_key,
835
- default_provider=default_provider,
836
- base_url=base_url,
837
- workers=workers,
838
- log_level=log_level,
839
- debug=debug,
840
- )
841
-
842
- def run_api(
843
- host: str = '0.0.0.0',
844
- port: int = None,
845
- api_key: str = None,
846
- default_provider: str = None,
847
- base_url: str = None,
848
- debug: bool = False,
849
- workers: int = 1,
850
- log_level: str = 'info',
851
- show_available_providers: bool = True,
852
- ) -> None:
853
- print("Starting Webscout OpenAI API server...")
854
- if port is None:
855
- port = DEFAULT_PORT
856
- AppConfig.set_config(
857
- api_key=api_key,
858
- default_provider=default_provider or AppConfig.default_provider,
859
- base_url=base_url
860
- )
861
- # initialize_provider_map() # This is called inside create_app now.
862
- # Call here if create_app doesn't exist yet or for early info.
863
- # For showing providers, it needs to be called before printing.
864
- if show_available_providers: # Initialize map if needed for display before app creation
865
- if not AppConfig.provider_map: # Avoid re-initializing if already done by app creation logic path
866
- initialize_provider_map()
867
-
868
- print("\n=== Webscout OpenAI API Server ===")
869
- print(f"Server URL: http://{host if host != '0.0.0.0' else 'localhost'}:{port}")
870
- if AppConfig.base_url:
871
- print(f"Base Path: {AppConfig.base_url}")
872
- api_endpoint_base = f"http://{host if host != '0.0.0.0' else 'localhost'}:{port}{AppConfig.base_url}"
873
- else:
874
- api_endpoint_base = f"http://{host if host != '0.0.0.0' else 'localhost'}:{port}"
875
-
876
- print(f"API Endpoint: {api_endpoint_base}/v1/chat/completions")
877
- print(f"Docs URL: {api_endpoint_base}/docs") # Adjusted for potential base_url in display
878
- print(f"API Authentication: {'Enabled' if api_key else 'Disabled'}")
879
- print(f"Default Provider: {AppConfig.default_provider}")
880
- print(f"Workers: {workers}")
881
- print(f"Log Level: {log_level}")
882
- print(f"Debug Mode: {'Enabled' if debug else 'Disabled'}")
883
-
884
- providers = list(set(v.__name__ for v in AppConfig.provider_map.values()))
885
- print(f"\n--- Available Providers ({len(providers)}) ---")
886
- for i, provider_name in enumerate(sorted(providers), 1):
887
- print(f"{i}. {provider_name}")
888
-
889
- provider_class_names = set(v.__name__ for v in AppConfig.provider_map.values())
890
- models = sorted([model for model in AppConfig.provider_map.keys() if model not in provider_class_names])
891
- if models:
892
- print(f"\n--- Available Models ({len(models)}) ---")
893
- for i, model_name in enumerate(models, 1):
894
- print(f"{i}. {model_name} (via {AppConfig.provider_map[model_name].__name__})")
895
- else:
896
- print("\nNo specific models registered. Use provider names as models.")
897
-
898
- print("\nUse Ctrl+C to stop the server.")
899
- print("=" * 40 + "\n")
900
-
901
- uvicorn_app_str = "webscout.Provider.OPENAI.api:create_app_debug" if debug else "webscout.Provider.OPENAI.api:create_app"
902
-
903
- # Configure uvicorn settings
904
- uvicorn_config = {
905
- "app": uvicorn_app_str,
906
- "host": host,
907
- "port": int(port),
908
- "factory": True,
909
- "reload": debug, # Enable reload only in debug mode for stability
910
- "log_level": log_level.lower() if log_level else ("debug" if debug else "info"),
911
- }
912
-
913
- # Add workers only if not in debug mode (reload and workers are incompatible)
914
- if not debug and workers > 1:
915
- uvicorn_config["workers"] = workers
916
- print(f"Starting with {workers} workers...")
917
- elif debug:
918
- print("Debug mode enabled - using single worker with reload...")
919
-
920
- # Note: Logs show "werkzeug". If /docs 404s persist, ensure Uvicorn is the actual server running.
921
- # The script uses uvicorn.run, so "werkzeug" logs are unexpected for this file.
922
- uvicorn.run(**uvicorn_config)
923
-
924
- if __name__ == "__main__":
925
- import argparse
926
-
927
- # Read environment variables with fallbacks
928
- default_port = int(os.getenv('WEBSCOUT_PORT', os.getenv('PORT', DEFAULT_PORT)))
929
- default_host = os.getenv('WEBSCOUT_HOST', DEFAULT_HOST)
930
- default_workers = int(os.getenv('WEBSCOUT_WORKERS', '1'))
931
- default_log_level = os.getenv('WEBSCOUT_LOG_LEVEL', 'info')
932
- default_api_key = os.getenv('WEBSCOUT_API_KEY', os.getenv('API_KEY'))
933
- default_provider = os.getenv('WEBSCOUT_DEFAULT_PROVIDER', os.getenv('DEFAULT_PROVIDER'))
934
- default_base_url = os.getenv('WEBSCOUT_BASE_URL', os.getenv('BASE_URL'))
935
- default_debug = os.getenv('WEBSCOUT_DEBUG', os.getenv('DEBUG', 'false')).lower() == 'true'
936
-
937
- parser = argparse.ArgumentParser(description='Start Webscout OpenAI-compatible API server')
938
- parser.add_argument('--port', type=int, default=default_port, help=f'Port to run the server on (default: {default_port})')
939
- parser.add_argument('--host', type=str, default=default_host, help=f'Host to bind the server to (default: {default_host})')
940
- parser.add_argument('--workers', type=int, default=default_workers, help=f'Number of worker processes (default: {default_workers})')
941
- 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})')
942
- parser.add_argument('--api-key', type=str, default=default_api_key, help='API key for authentication (optional)')
943
- parser.add_argument('--default-provider', type=str, default=default_provider, help='Default provider to use (optional)')
944
- parser.add_argument('--base-url', type=str, default=default_base_url, help='Base URL for the API (optional, e.g., /api/v1)')
945
- parser.add_argument('--debug', action='store_true', default=default_debug, help='Run in debug mode')
946
- args = parser.parse_args()
947
-
948
- # Print configuration summary
949
- print(f"Configuration:")
950
- print(f" Host: {args.host}")
951
- print(f" Port: {args.port}")
952
- print(f" Workers: {args.workers}")
953
- print(f" Log Level: {args.log_level}")
954
- print(f" Debug Mode: {args.debug}")
955
- print(f" API Key: {'Set' if args.api_key else 'Not set'}")
956
- print(f" Default Provider: {args.default_provider or 'Not set'}")
957
- print(f" Base URL: {args.base_url or 'Not set'}")
958
- print()
959
-
960
- run_api(
961
- host=args.host,
962
- port=args.port,
963
- workers=args.workers,
964
- log_level=args.log_level,
965
- api_key=args.api_key,
966
- default_provider=args.default_provider,
967
- base_url=args.base_url,
968
- debug=args.debug
969
- )