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.
- webscout/AIauto.py +524 -251
- webscout/AIbase.py +247 -319
- webscout/AIutel.py +68 -703
- webscout/Bard.py +1072 -1026
- webscout/Extra/GitToolkit/__init__.py +10 -10
- webscout/Extra/GitToolkit/gitapi/__init__.py +20 -12
- webscout/Extra/GitToolkit/gitapi/gist.py +142 -0
- webscout/Extra/GitToolkit/gitapi/organization.py +91 -0
- webscout/Extra/GitToolkit/gitapi/repository.py +308 -195
- webscout/Extra/GitToolkit/gitapi/search.py +162 -0
- webscout/Extra/GitToolkit/gitapi/trending.py +236 -0
- webscout/Extra/GitToolkit/gitapi/user.py +128 -96
- webscout/Extra/GitToolkit/gitapi/utils.py +82 -62
- webscout/Extra/YTToolkit/README.md +443 -375
- webscout/Extra/YTToolkit/YTdownloader.py +953 -957
- webscout/Extra/YTToolkit/__init__.py +3 -3
- webscout/Extra/YTToolkit/transcriber.py +595 -476
- webscout/Extra/YTToolkit/ytapi/README.md +230 -44
- webscout/Extra/YTToolkit/ytapi/__init__.py +22 -6
- webscout/Extra/YTToolkit/ytapi/captions.py +190 -0
- webscout/Extra/YTToolkit/ytapi/channel.py +302 -307
- webscout/Extra/YTToolkit/ytapi/errors.py +13 -13
- webscout/Extra/YTToolkit/ytapi/extras.py +178 -118
- webscout/Extra/YTToolkit/ytapi/hashtag.py +120 -0
- webscout/Extra/YTToolkit/ytapi/https.py +89 -88
- webscout/Extra/YTToolkit/ytapi/patterns.py +61 -61
- webscout/Extra/YTToolkit/ytapi/playlist.py +59 -59
- webscout/Extra/YTToolkit/ytapi/pool.py +8 -8
- webscout/Extra/YTToolkit/ytapi/query.py +143 -40
- webscout/Extra/YTToolkit/ytapi/shorts.py +122 -0
- webscout/Extra/YTToolkit/ytapi/stream.py +68 -63
- webscout/Extra/YTToolkit/ytapi/suggestions.py +97 -0
- webscout/Extra/YTToolkit/ytapi/utils.py +66 -62
- webscout/Extra/YTToolkit/ytapi/video.py +403 -232
- webscout/Extra/__init__.py +2 -3
- webscout/Extra/gguf.py +1298 -684
- webscout/Extra/tempmail/README.md +487 -487
- webscout/Extra/tempmail/__init__.py +28 -28
- webscout/Extra/tempmail/async_utils.py +143 -141
- webscout/Extra/tempmail/base.py +172 -161
- webscout/Extra/tempmail/cli.py +191 -187
- webscout/Extra/tempmail/emailnator.py +88 -84
- webscout/Extra/tempmail/mail_tm.py +378 -361
- webscout/Extra/tempmail/temp_mail_io.py +304 -292
- webscout/Extra/weather.py +196 -194
- webscout/Extra/weather_ascii.py +17 -15
- webscout/Provider/AISEARCH/PERPLEXED_search.py +175 -0
- webscout/Provider/AISEARCH/Perplexity.py +292 -333
- webscout/Provider/AISEARCH/README.md +106 -279
- webscout/Provider/AISEARCH/__init__.py +16 -9
- webscout/Provider/AISEARCH/brave_search.py +298 -0
- webscout/Provider/AISEARCH/iask_search.py +357 -410
- webscout/Provider/AISEARCH/monica_search.py +200 -220
- webscout/Provider/AISEARCH/webpilotai_search.py +242 -255
- webscout/Provider/Algion.py +413 -0
- webscout/Provider/Andi.py +74 -69
- webscout/Provider/Apriel.py +313 -0
- webscout/Provider/Ayle.py +323 -0
- webscout/Provider/ChatSandbox.py +329 -342
- webscout/Provider/ClaudeOnline.py +365 -0
- webscout/Provider/Cohere.py +232 -208
- webscout/Provider/DeepAI.py +367 -0
- webscout/Provider/Deepinfra.py +467 -340
- webscout/Provider/EssentialAI.py +217 -0
- webscout/Provider/ExaAI.py +274 -261
- webscout/Provider/Gemini.py +175 -169
- webscout/Provider/GithubChat.py +385 -369
- webscout/Provider/Gradient.py +286 -0
- webscout/Provider/Groq.py +556 -801
- webscout/Provider/HadadXYZ.py +323 -0
- webscout/Provider/HeckAI.py +392 -375
- webscout/Provider/HuggingFace.py +387 -0
- webscout/Provider/IBM.py +340 -0
- webscout/Provider/Jadve.py +317 -291
- webscout/Provider/K2Think.py +306 -0
- webscout/Provider/Koboldai.py +221 -384
- webscout/Provider/Netwrck.py +273 -270
- webscout/Provider/Nvidia.py +310 -0
- webscout/Provider/OPENAI/DeepAI.py +489 -0
- webscout/Provider/OPENAI/K2Think.py +423 -0
- webscout/Provider/OPENAI/PI.py +463 -0
- webscout/Provider/OPENAI/README.md +890 -952
- webscout/Provider/OPENAI/TogetherAI.py +405 -0
- webscout/Provider/OPENAI/TwoAI.py +255 -357
- webscout/Provider/OPENAI/__init__.py +148 -40
- webscout/Provider/OPENAI/ai4chat.py +348 -293
- webscout/Provider/OPENAI/akashgpt.py +436 -0
- webscout/Provider/OPENAI/algion.py +303 -0
- webscout/Provider/OPENAI/{exachat.py → ayle.py} +365 -444
- webscout/Provider/OPENAI/base.py +253 -249
- webscout/Provider/OPENAI/cerebras.py +296 -0
- webscout/Provider/OPENAI/chatgpt.py +870 -556
- webscout/Provider/OPENAI/chatsandbox.py +233 -173
- webscout/Provider/OPENAI/deepinfra.py +403 -322
- webscout/Provider/OPENAI/e2b.py +2370 -1414
- webscout/Provider/OPENAI/elmo.py +278 -0
- webscout/Provider/OPENAI/exaai.py +452 -417
- webscout/Provider/OPENAI/freeassist.py +446 -0
- webscout/Provider/OPENAI/gradient.py +448 -0
- webscout/Provider/OPENAI/groq.py +380 -364
- webscout/Provider/OPENAI/hadadxyz.py +292 -0
- webscout/Provider/OPENAI/heckai.py +333 -308
- webscout/Provider/OPENAI/huggingface.py +321 -0
- webscout/Provider/OPENAI/ibm.py +425 -0
- webscout/Provider/OPENAI/llmchat.py +253 -0
- webscout/Provider/OPENAI/llmchatco.py +378 -335
- webscout/Provider/OPENAI/meta.py +541 -0
- webscout/Provider/OPENAI/netwrck.py +374 -357
- webscout/Provider/OPENAI/nvidia.py +317 -0
- webscout/Provider/OPENAI/oivscode.py +348 -287
- webscout/Provider/OPENAI/openrouter.py +328 -0
- webscout/Provider/OPENAI/pydantic_imports.py +1 -172
- webscout/Provider/OPENAI/sambanova.py +397 -0
- webscout/Provider/OPENAI/sonus.py +305 -304
- webscout/Provider/OPENAI/textpollinations.py +370 -339
- webscout/Provider/OPENAI/toolbaz.py +375 -413
- webscout/Provider/OPENAI/typefully.py +419 -355
- webscout/Provider/OPENAI/typliai.py +279 -0
- webscout/Provider/OPENAI/utils.py +314 -318
- webscout/Provider/OPENAI/wisecat.py +359 -387
- webscout/Provider/OPENAI/writecream.py +185 -163
- webscout/Provider/OPENAI/x0gpt.py +462 -365
- webscout/Provider/OPENAI/zenmux.py +380 -0
- webscout/Provider/OpenRouter.py +386 -0
- webscout/Provider/Openai.py +337 -496
- webscout/Provider/PI.py +443 -429
- webscout/Provider/QwenLM.py +346 -254
- webscout/Provider/STT/__init__.py +28 -0
- webscout/Provider/STT/base.py +303 -0
- webscout/Provider/STT/elevenlabs.py +264 -0
- webscout/Provider/Sambanova.py +317 -0
- webscout/Provider/TTI/README.md +69 -82
- webscout/Provider/TTI/__init__.py +37 -7
- webscout/Provider/TTI/base.py +147 -64
- webscout/Provider/TTI/claudeonline.py +393 -0
- webscout/Provider/TTI/magicstudio.py +292 -201
- webscout/Provider/TTI/miragic.py +180 -0
- webscout/Provider/TTI/pollinations.py +331 -221
- webscout/Provider/TTI/together.py +334 -0
- webscout/Provider/TTI/utils.py +14 -11
- webscout/Provider/TTS/README.md +186 -192
- webscout/Provider/TTS/__init__.py +43 -10
- webscout/Provider/TTS/base.py +523 -159
- webscout/Provider/TTS/deepgram.py +286 -156
- webscout/Provider/TTS/elevenlabs.py +189 -111
- webscout/Provider/TTS/freetts.py +218 -0
- webscout/Provider/TTS/murfai.py +288 -113
- webscout/Provider/TTS/openai_fm.py +364 -129
- webscout/Provider/TTS/parler.py +203 -111
- webscout/Provider/TTS/qwen.py +334 -0
- webscout/Provider/TTS/sherpa.py +286 -0
- webscout/Provider/TTS/speechma.py +693 -580
- webscout/Provider/TTS/streamElements.py +275 -333
- webscout/Provider/TTS/utils.py +280 -280
- webscout/Provider/TextPollinationsAI.py +331 -308
- webscout/Provider/TogetherAI.py +450 -0
- webscout/Provider/TwoAI.py +309 -475
- webscout/Provider/TypliAI.py +311 -305
- webscout/Provider/UNFINISHED/ChatHub.py +219 -209
- webscout/Provider/{OPENAI/glider.py → UNFINISHED/ChutesAI.py} +331 -326
- webscout/Provider/{GizAI.py → UNFINISHED/GizAI.py} +300 -295
- webscout/Provider/{Marcus.py → UNFINISHED/Marcus.py} +218 -198
- webscout/Provider/UNFINISHED/Qodo.py +481 -0
- webscout/Provider/{MCPCore.py → UNFINISHED/XenAI.py} +330 -315
- webscout/Provider/UNFINISHED/Youchat.py +347 -330
- webscout/Provider/UNFINISHED/aihumanizer.py +41 -0
- webscout/Provider/UNFINISHED/grammerchecker.py +37 -0
- webscout/Provider/UNFINISHED/liner.py +342 -0
- webscout/Provider/UNFINISHED/liner_api_request.py +246 -263
- webscout/Provider/{samurai.py → UNFINISHED/samurai.py} +231 -224
- webscout/Provider/WiseCat.py +256 -233
- webscout/Provider/WrDoChat.py +390 -370
- webscout/Provider/__init__.py +115 -174
- webscout/Provider/ai4chat.py +181 -174
- webscout/Provider/akashgpt.py +330 -335
- webscout/Provider/cerebras.py +397 -290
- webscout/Provider/cleeai.py +236 -213
- webscout/Provider/elmo.py +291 -283
- webscout/Provider/geminiapi.py +343 -208
- webscout/Provider/julius.py +245 -223
- webscout/Provider/learnfastai.py +333 -325
- webscout/Provider/llama3mitril.py +230 -215
- webscout/Provider/llmchat.py +308 -258
- webscout/Provider/llmchatco.py +321 -306
- webscout/Provider/meta.py +996 -801
- webscout/Provider/oivscode.py +332 -309
- webscout/Provider/searchchat.py +316 -292
- webscout/Provider/sonus.py +264 -258
- webscout/Provider/toolbaz.py +359 -353
- webscout/Provider/turboseek.py +332 -266
- webscout/Provider/typefully.py +262 -202
- webscout/Provider/x0gpt.py +332 -299
- webscout/__init__.py +31 -39
- webscout/__main__.py +5 -5
- webscout/cli.py +585 -524
- webscout/client.py +1497 -70
- webscout/conversation.py +140 -436
- webscout/exceptions.py +383 -362
- webscout/litagent/__init__.py +29 -29
- webscout/litagent/agent.py +492 -455
- webscout/litagent/constants.py +60 -60
- webscout/models.py +505 -181
- webscout/optimizers.py +74 -420
- webscout/prompt_manager.py +376 -288
- webscout/sanitize.py +1514 -0
- webscout/scout/README.md +452 -404
- webscout/scout/__init__.py +8 -8
- webscout/scout/core/__init__.py +7 -7
- webscout/scout/core/crawler.py +330 -210
- webscout/scout/core/scout.py +800 -607
- webscout/scout/core/search_result.py +51 -96
- webscout/scout/core/text_analyzer.py +64 -63
- webscout/scout/core/text_utils.py +412 -277
- webscout/scout/core/web_analyzer.py +54 -52
- webscout/scout/element.py +872 -478
- webscout/scout/parsers/__init__.py +70 -69
- webscout/scout/parsers/html5lib_parser.py +182 -172
- webscout/scout/parsers/html_parser.py +238 -236
- webscout/scout/parsers/lxml_parser.py +203 -178
- webscout/scout/utils.py +38 -37
- webscout/search/__init__.py +47 -0
- webscout/search/base.py +201 -0
- webscout/search/bing_main.py +45 -0
- webscout/search/brave_main.py +92 -0
- webscout/search/duckduckgo_main.py +57 -0
- webscout/search/engines/__init__.py +127 -0
- webscout/search/engines/bing/__init__.py +15 -0
- webscout/search/engines/bing/base.py +35 -0
- webscout/search/engines/bing/images.py +114 -0
- webscout/search/engines/bing/news.py +96 -0
- webscout/search/engines/bing/suggestions.py +36 -0
- webscout/search/engines/bing/text.py +109 -0
- webscout/search/engines/brave/__init__.py +19 -0
- webscout/search/engines/brave/base.py +47 -0
- webscout/search/engines/brave/images.py +213 -0
- webscout/search/engines/brave/news.py +353 -0
- webscout/search/engines/brave/suggestions.py +318 -0
- webscout/search/engines/brave/text.py +167 -0
- webscout/search/engines/brave/videos.py +364 -0
- webscout/search/engines/duckduckgo/__init__.py +25 -0
- webscout/search/engines/duckduckgo/answers.py +80 -0
- webscout/search/engines/duckduckgo/base.py +189 -0
- webscout/search/engines/duckduckgo/images.py +100 -0
- webscout/search/engines/duckduckgo/maps.py +183 -0
- webscout/search/engines/duckduckgo/news.py +70 -0
- webscout/search/engines/duckduckgo/suggestions.py +22 -0
- webscout/search/engines/duckduckgo/text.py +221 -0
- webscout/search/engines/duckduckgo/translate.py +48 -0
- webscout/search/engines/duckduckgo/videos.py +80 -0
- webscout/search/engines/duckduckgo/weather.py +84 -0
- webscout/search/engines/mojeek.py +61 -0
- webscout/search/engines/wikipedia.py +77 -0
- webscout/search/engines/yahoo/__init__.py +41 -0
- webscout/search/engines/yahoo/answers.py +19 -0
- webscout/search/engines/yahoo/base.py +34 -0
- webscout/search/engines/yahoo/images.py +323 -0
- webscout/search/engines/yahoo/maps.py +19 -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 +19 -0
- webscout/search/engines/yahoo/videos.py +302 -0
- webscout/search/engines/yahoo/weather.py +220 -0
- webscout/search/engines/yandex.py +67 -0
- webscout/search/engines/yep/__init__.py +13 -0
- webscout/search/engines/yep/base.py +34 -0
- webscout/search/engines/yep/images.py +101 -0
- webscout/search/engines/yep/suggestions.py +38 -0
- webscout/search/engines/yep/text.py +99 -0
- webscout/search/http_client.py +172 -0
- webscout/search/results.py +141 -0
- webscout/search/yahoo_main.py +57 -0
- webscout/search/yep_main.py +48 -0
- webscout/server/__init__.py +48 -0
- webscout/server/config.py +78 -0
- webscout/server/exceptions.py +69 -0
- webscout/server/providers.py +286 -0
- webscout/server/request_models.py +131 -0
- webscout/server/request_processing.py +404 -0
- webscout/server/routes.py +642 -0
- webscout/server/server.py +351 -0
- webscout/server/ui_templates.py +1171 -0
- webscout/swiftcli/__init__.py +79 -95
- webscout/swiftcli/core/__init__.py +7 -7
- webscout/swiftcli/core/cli.py +574 -297
- webscout/swiftcli/core/context.py +98 -104
- webscout/swiftcli/core/group.py +268 -241
- webscout/swiftcli/decorators/__init__.py +28 -28
- webscout/swiftcli/decorators/command.py +243 -221
- webscout/swiftcli/decorators/options.py +247 -220
- webscout/swiftcli/decorators/output.py +392 -252
- webscout/swiftcli/exceptions.py +21 -21
- webscout/swiftcli/plugins/__init__.py +9 -9
- webscout/swiftcli/plugins/base.py +134 -135
- webscout/swiftcli/plugins/manager.py +269 -269
- webscout/swiftcli/utils/__init__.py +58 -59
- webscout/swiftcli/utils/formatting.py +251 -252
- webscout/swiftcli/utils/parsing.py +368 -267
- webscout/update_checker.py +280 -136
- webscout/utils.py +28 -14
- webscout/version.py +2 -1
- webscout/version.py.bak +3 -0
- webscout/zeroart/__init__.py +218 -135
- webscout/zeroart/base.py +70 -66
- webscout/zeroart/effects.py +155 -101
- webscout/zeroart/fonts.py +1799 -1239
- webscout-2026.1.19.dist-info/METADATA +638 -0
- webscout-2026.1.19.dist-info/RECORD +312 -0
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/WHEEL +1 -1
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/entry_points.txt +1 -1
- webscout/DWEBS.py +0 -520
- webscout/Extra/Act.md +0 -309
- webscout/Extra/GitToolkit/gitapi/README.md +0 -110
- webscout/Extra/autocoder/__init__.py +0 -9
- webscout/Extra/autocoder/autocoder.py +0 -1105
- webscout/Extra/autocoder/autocoder_utiles.py +0 -332
- webscout/Extra/gguf.md +0 -430
- webscout/Extra/weather.md +0 -281
- webscout/Litlogger/README.md +0 -10
- webscout/Litlogger/__init__.py +0 -15
- webscout/Litlogger/formats.py +0 -4
- webscout/Litlogger/handlers.py +0 -103
- webscout/Litlogger/levels.py +0 -13
- webscout/Litlogger/logger.py +0 -92
- webscout/Provider/AI21.py +0 -177
- webscout/Provider/AISEARCH/DeepFind.py +0 -254
- webscout/Provider/AISEARCH/felo_search.py +0 -202
- webscout/Provider/AISEARCH/genspark_search.py +0 -324
- webscout/Provider/AISEARCH/hika_search.py +0 -186
- webscout/Provider/AISEARCH/scira_search.py +0 -298
- webscout/Provider/Aitopia.py +0 -316
- webscout/Provider/AllenAI.py +0 -440
- webscout/Provider/Blackboxai.py +0 -791
- webscout/Provider/ChatGPTClone.py +0 -237
- webscout/Provider/ChatGPTGratis.py +0 -194
- webscout/Provider/Cloudflare.py +0 -324
- webscout/Provider/ExaChat.py +0 -358
- webscout/Provider/Flowith.py +0 -217
- webscout/Provider/FreeGemini.py +0 -250
- webscout/Provider/Glider.py +0 -225
- webscout/Provider/HF_space/__init__.py +0 -0
- webscout/Provider/HF_space/qwen_qwen2.py +0 -206
- webscout/Provider/HuggingFaceChat.py +0 -469
- webscout/Provider/Hunyuan.py +0 -283
- webscout/Provider/LambdaChat.py +0 -411
- webscout/Provider/Llama3.py +0 -259
- webscout/Provider/Nemotron.py +0 -218
- webscout/Provider/OLLAMA.py +0 -396
- webscout/Provider/OPENAI/BLACKBOXAI.py +0 -766
- webscout/Provider/OPENAI/Cloudflare.py +0 -378
- webscout/Provider/OPENAI/FreeGemini.py +0 -283
- webscout/Provider/OPENAI/NEMOTRON.py +0 -232
- webscout/Provider/OPENAI/Qwen3.py +0 -283
- webscout/Provider/OPENAI/api.py +0 -969
- webscout/Provider/OPENAI/c4ai.py +0 -373
- webscout/Provider/OPENAI/chatgptclone.py +0 -494
- webscout/Provider/OPENAI/copilot.py +0 -242
- webscout/Provider/OPENAI/flowith.py +0 -162
- webscout/Provider/OPENAI/freeaichat.py +0 -359
- webscout/Provider/OPENAI/mcpcore.py +0 -389
- webscout/Provider/OPENAI/multichat.py +0 -376
- webscout/Provider/OPENAI/opkfc.py +0 -496
- webscout/Provider/OPENAI/scirachat.py +0 -477
- webscout/Provider/OPENAI/standardinput.py +0 -433
- webscout/Provider/OPENAI/typegpt.py +0 -364
- webscout/Provider/OPENAI/uncovrAI.py +0 -463
- webscout/Provider/OPENAI/venice.py +0 -431
- webscout/Provider/OPENAI/yep.py +0 -382
- webscout/Provider/OpenGPT.py +0 -209
- webscout/Provider/Perplexitylabs.py +0 -415
- webscout/Provider/Reka.py +0 -214
- webscout/Provider/StandardInput.py +0 -290
- webscout/Provider/TTI/aiarta.py +0 -365
- webscout/Provider/TTI/artbit.py +0 -0
- webscout/Provider/TTI/fastflux.py +0 -200
- webscout/Provider/TTI/piclumen.py +0 -203
- webscout/Provider/TTI/pixelmuse.py +0 -225
- webscout/Provider/TTS/gesserit.py +0 -128
- webscout/Provider/TTS/sthir.py +0 -94
- webscout/Provider/TeachAnything.py +0 -229
- webscout/Provider/UNFINISHED/puterjs.py +0 -635
- webscout/Provider/UNFINISHED/test_lmarena.py +0 -119
- webscout/Provider/Venice.py +0 -258
- webscout/Provider/VercelAI.py +0 -253
- webscout/Provider/Writecream.py +0 -246
- webscout/Provider/WritingMate.py +0 -269
- webscout/Provider/asksteve.py +0 -220
- webscout/Provider/chatglm.py +0 -215
- webscout/Provider/copilot.py +0 -425
- webscout/Provider/freeaichat.py +0 -285
- webscout/Provider/granite.py +0 -235
- webscout/Provider/hermes.py +0 -266
- webscout/Provider/koala.py +0 -170
- webscout/Provider/lmarena.py +0 -198
- webscout/Provider/multichat.py +0 -364
- webscout/Provider/scira_chat.py +0 -299
- webscout/Provider/scnet.py +0 -243
- webscout/Provider/talkai.py +0 -194
- webscout/Provider/typegpt.py +0 -289
- webscout/Provider/uncovr.py +0 -368
- webscout/Provider/yep.py +0 -389
- webscout/litagent/Readme.md +0 -276
- webscout/litprinter/__init__.py +0 -59
- webscout/swiftcli/Readme.md +0 -323
- webscout/tempid.py +0 -128
- webscout/webscout_search.py +0 -1184
- webscout/webscout_search_async.py +0 -654
- webscout/yep_search.py +0 -347
- webscout/zeroart/README.md +0 -89
- webscout-8.2.9.dist-info/METADATA +0 -1033
- webscout-8.2.9.dist-info/RECORD +0 -289
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/top_level.txt +0 -0
webscout/client.py
CHANGED
|
@@ -1,70 +1,1497 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Unified
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
from webscout.Provider.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
"""
|
|
2
|
+
Webscout Unified Client Interface
|
|
3
|
+
|
|
4
|
+
A unified client for webscout that provides a simple interface
|
|
5
|
+
to interact with multiple AI providers for chat completions and image generation.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Automatic provider failover
|
|
9
|
+
- Support for specifying exact provider
|
|
10
|
+
- Intelligent model resolution (auto, provider/model, or model name)
|
|
11
|
+
- Caching of provider instances
|
|
12
|
+
- Full streaming support
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import difflib
|
|
16
|
+
import importlib
|
|
17
|
+
import inspect
|
|
18
|
+
import pkgutil
|
|
19
|
+
import random
|
|
20
|
+
from typing import Any, Dict, Generator, List, Optional, Set, Tuple, Type, Union, cast
|
|
21
|
+
|
|
22
|
+
from webscout.Provider.OPENAI.base import (
|
|
23
|
+
BaseChat,
|
|
24
|
+
BaseCompletions,
|
|
25
|
+
OpenAICompatibleProvider,
|
|
26
|
+
Tool,
|
|
27
|
+
)
|
|
28
|
+
from webscout.Provider.OPENAI.utils import (
|
|
29
|
+
ChatCompletion,
|
|
30
|
+
ChatCompletionChunk,
|
|
31
|
+
)
|
|
32
|
+
from webscout.Provider.TTI.base import BaseImages, TTICompatibleProvider
|
|
33
|
+
from webscout.Provider.TTI.utils import ImageResponse
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_openai_providers() -> Tuple[Dict[str, Type[OpenAICompatibleProvider]], Set[str]]:
|
|
37
|
+
"""
|
|
38
|
+
Dynamically loads all OpenAI-compatible provider classes from the OPENAI module.
|
|
39
|
+
|
|
40
|
+
Scans the webscout.Provider.OPENAI package and imports all subclasses of
|
|
41
|
+
OpenAICompatibleProvider. Excludes base classes, utility modules, and private classes.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A tuple containing:
|
|
45
|
+
- A dictionary mapping provider class names to their class objects.
|
|
46
|
+
- A set of provider names that require API authentication.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
No exceptions are raised; failures are silently handled to ensure robust loading.
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
>>> providers, auth_required = load_openai_providers()
|
|
53
|
+
>>> print(list(providers.keys())[:3])
|
|
54
|
+
['Claude', 'GPT4Free', 'OpenRouter']
|
|
55
|
+
>>> print('Claude' in auth_required)
|
|
56
|
+
True
|
|
57
|
+
"""
|
|
58
|
+
provider_map = {}
|
|
59
|
+
auth_required_providers = set()
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
provider_package = importlib.import_module("webscout.Provider.OPENAI")
|
|
63
|
+
for _, module_name, _ in pkgutil.iter_modules(provider_package.__path__):
|
|
64
|
+
if module_name.startswith(("base", "utils", "pydantic", "__")):
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
module = importlib.import_module(f"webscout.Provider.OPENAI.{module_name}")
|
|
68
|
+
for attr_name in dir(module):
|
|
69
|
+
attr = getattr(module, attr_name)
|
|
70
|
+
if (
|
|
71
|
+
isinstance(attr, type)
|
|
72
|
+
and issubclass(attr, OpenAICompatibleProvider)
|
|
73
|
+
and attr != OpenAICompatibleProvider
|
|
74
|
+
and not attr_name.startswith(("Base", "_"))
|
|
75
|
+
):
|
|
76
|
+
provider_map[attr_name] = attr
|
|
77
|
+
if hasattr(attr, "required_auth") and attr.required_auth:
|
|
78
|
+
auth_required_providers.add(attr_name)
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
return provider_map, auth_required_providers
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def load_tti_providers() -> Tuple[Dict[str, Type[TTICompatibleProvider]], Set[str]]:
|
|
87
|
+
"""
|
|
88
|
+
Dynamically loads all TTI (Text-to-Image) provider classes from the TTI module.
|
|
89
|
+
|
|
90
|
+
Scans the webscout.Provider.TTI package and imports all subclasses of
|
|
91
|
+
TTICompatibleProvider. Excludes base classes, utility modules, and private classes.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A tuple containing:
|
|
95
|
+
- A dictionary mapping TTI provider class names to their class objects.
|
|
96
|
+
- A set of TTI provider names that require API authentication.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
No exceptions are raised; failures are silently handled to ensure robust loading.
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
>>> providers, auth_required = load_tti_providers()
|
|
103
|
+
>>> print('DALL-E' in providers)
|
|
104
|
+
True
|
|
105
|
+
>>> print('Stable Diffusion' in auth_required)
|
|
106
|
+
False
|
|
107
|
+
"""
|
|
108
|
+
provider_map = {}
|
|
109
|
+
auth_required_providers = set()
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
provider_package = importlib.import_module("webscout.Provider.TTI")
|
|
113
|
+
for _, module_name, _ in pkgutil.iter_modules(provider_package.__path__):
|
|
114
|
+
if module_name.startswith(("base", "utils", "__")):
|
|
115
|
+
continue
|
|
116
|
+
try:
|
|
117
|
+
module = importlib.import_module(f"webscout.Provider.TTI.{module_name}")
|
|
118
|
+
for attr_name in dir(module):
|
|
119
|
+
attr = getattr(module, attr_name)
|
|
120
|
+
if (
|
|
121
|
+
isinstance(attr, type)
|
|
122
|
+
and issubclass(attr, TTICompatibleProvider)
|
|
123
|
+
and attr != TTICompatibleProvider
|
|
124
|
+
and not attr_name.startswith(("Base", "_"))
|
|
125
|
+
):
|
|
126
|
+
provider_map[attr_name] = attr
|
|
127
|
+
if hasattr(attr, "required_auth") and attr.required_auth:
|
|
128
|
+
auth_required_providers.add(attr_name)
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
return provider_map, auth_required_providers
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
OPENAI_PROVIDERS, OPENAI_AUTH_REQUIRED = load_openai_providers()
|
|
137
|
+
TTI_PROVIDERS, TTI_AUTH_REQUIRED = load_tti_providers()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _get_models_safely(provider_cls: type, client: Optional["Client"] = None) -> List[str]:
|
|
141
|
+
"""
|
|
142
|
+
Safely retrieves the list of available models from a provider.
|
|
143
|
+
|
|
144
|
+
Attempts to instantiate the provider class and call its models.list() method.
|
|
145
|
+
If a Client instance is provided, uses the client's provider cache to avoid
|
|
146
|
+
redundant instantiations. Handles all exceptions gracefully and returns an
|
|
147
|
+
empty list if model retrieval fails.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
provider_cls: The provider class to retrieve models from.
|
|
151
|
+
client: Optional Client instance to use for caching and configuration.
|
|
152
|
+
If provided, uses client's proxies and api_key for initialization.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
A list of available model identifiers (strings). Returns an empty list
|
|
156
|
+
if the provider has no models or if instantiation fails.
|
|
157
|
+
|
|
158
|
+
Note:
|
|
159
|
+
This function silently handles all exceptions and will not raise errors.
|
|
160
|
+
Model names are extracted from both string lists and dicts with 'id' keys.
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
>>> from webscout.client import _get_models_safely, Client
|
|
164
|
+
>>> client = Client()
|
|
165
|
+
>>> from webscout.Provider.OPENAI.some_provider import SomeProvider
|
|
166
|
+
>>> models = _get_models_safely(SomeProvider, client)
|
|
167
|
+
>>> print(models)
|
|
168
|
+
['gpt-4', 'gpt-3.5-turbo']
|
|
169
|
+
"""
|
|
170
|
+
models = []
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
instance = None
|
|
174
|
+
if client:
|
|
175
|
+
p_name = provider_cls.__name__
|
|
176
|
+
if p_name in client._provider_cache:
|
|
177
|
+
instance = client._provider_cache[p_name]
|
|
178
|
+
else:
|
|
179
|
+
try:
|
|
180
|
+
init_kwargs = {}
|
|
181
|
+
if client.proxies:
|
|
182
|
+
init_kwargs["proxies"] = client.proxies
|
|
183
|
+
if client.api_key:
|
|
184
|
+
init_kwargs["api_key"] = client.api_key
|
|
185
|
+
instance = provider_cls(**init_kwargs)
|
|
186
|
+
except Exception:
|
|
187
|
+
try:
|
|
188
|
+
instance = provider_cls()
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
if instance:
|
|
193
|
+
client._provider_cache[p_name] = instance
|
|
194
|
+
else:
|
|
195
|
+
try:
|
|
196
|
+
instance = provider_cls()
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
if instance and hasattr(instance, "models") and hasattr(instance.models, "list"):
|
|
201
|
+
res = instance.models.list()
|
|
202
|
+
if isinstance(res, list):
|
|
203
|
+
for m in res:
|
|
204
|
+
if isinstance(m, str):
|
|
205
|
+
models.append(m)
|
|
206
|
+
elif isinstance(m, dict) and "id" in m:
|
|
207
|
+
models.append(m["id"])
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
return models
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class ClientCompletions(BaseCompletions):
|
|
215
|
+
"""
|
|
216
|
+
Unified completions interface with intelligent provider and model resolution.
|
|
217
|
+
|
|
218
|
+
This class manages chat completions by automatically selecting appropriate
|
|
219
|
+
providers and models based on user input. It supports:
|
|
220
|
+
- Automatic model discovery and fuzzy matching
|
|
221
|
+
- Provider failover for reliability
|
|
222
|
+
- Provider and model caching for performance
|
|
223
|
+
- Streaming and non-streaming responses
|
|
224
|
+
- Tools and function calling support
|
|
225
|
+
|
|
226
|
+
Attributes:
|
|
227
|
+
_client: Reference to the parent Client instance.
|
|
228
|
+
_last_provider: Name of the last successfully used provider.
|
|
229
|
+
|
|
230
|
+
Examples:
|
|
231
|
+
>>> from webscout.client import Client
|
|
232
|
+
>>> client = Client(print_provider_info=True)
|
|
233
|
+
>>> response = client.chat.completions.create(
|
|
234
|
+
... model="auto",
|
|
235
|
+
... messages=[{"role": "user", "content": "Hello!"}]
|
|
236
|
+
... )
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def __init__(self, client: "Client"):
|
|
240
|
+
self._client = client
|
|
241
|
+
self._last_provider: Optional[str] = None
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def last_provider(self) -> Optional[str]:
|
|
245
|
+
"""
|
|
246
|
+
Returns the name of the last successfully used provider.
|
|
247
|
+
|
|
248
|
+
This property tracks which provider was most recently used to generate
|
|
249
|
+
a completion. Useful for debugging and understanding which fallback
|
|
250
|
+
providers are being utilized.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
The name of the last provider as a string, or None if no provider
|
|
254
|
+
has been successfully used yet.
|
|
255
|
+
|
|
256
|
+
Examples:
|
|
257
|
+
>>> completions = client.chat.completions
|
|
258
|
+
>>> response = completions.create(model="auto", messages=[...])
|
|
259
|
+
>>> print(completions.last_provider)
|
|
260
|
+
'GPT4Free'
|
|
261
|
+
"""
|
|
262
|
+
return self._last_provider
|
|
263
|
+
|
|
264
|
+
def _get_provider_instance(
|
|
265
|
+
self, provider_class: Type[OpenAICompatibleProvider], **kwargs
|
|
266
|
+
) -> OpenAICompatibleProvider:
|
|
267
|
+
"""
|
|
268
|
+
Retrieves or creates a cached provider instance.
|
|
269
|
+
|
|
270
|
+
Checks if a provider instance already exists in the client's cache.
|
|
271
|
+
If not, initializes a new instance with client-level configuration
|
|
272
|
+
(proxies, api_key) merged with any additional kwargs.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
provider_class: The OpenAI-compatible provider class to instantiate.
|
|
276
|
+
**kwargs: Additional keyword arguments to pass to the provider's constructor.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
An instantiated and initialized provider instance.
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
RuntimeError: If the provider cannot be initialized with or without
|
|
283
|
+
client configuration.
|
|
284
|
+
|
|
285
|
+
Examples:
|
|
286
|
+
>>> from webscout.Provider.OPENAI.gpt4free import GPT4Free
|
|
287
|
+
>>> completions = client.chat.completions
|
|
288
|
+
>>> instance = completions._get_provider_instance(GPT4Free)
|
|
289
|
+
"""
|
|
290
|
+
p_name = provider_class.__name__
|
|
291
|
+
if p_name in self._client._provider_cache:
|
|
292
|
+
return self._client._provider_cache[p_name]
|
|
293
|
+
|
|
294
|
+
init_kwargs = {}
|
|
295
|
+
if self._client.proxies:
|
|
296
|
+
init_kwargs["proxies"] = self._client.proxies
|
|
297
|
+
if self._client.api_key:
|
|
298
|
+
init_kwargs["api_key"] = self._client.api_key
|
|
299
|
+
init_kwargs.update(kwargs)
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
instance = provider_class(**init_kwargs)
|
|
303
|
+
self._client._provider_cache[p_name] = instance
|
|
304
|
+
return instance
|
|
305
|
+
except Exception:
|
|
306
|
+
try:
|
|
307
|
+
instance = provider_class()
|
|
308
|
+
self._client._provider_cache[p_name] = instance
|
|
309
|
+
return instance
|
|
310
|
+
except Exception as e:
|
|
311
|
+
raise RuntimeError(f"Failed to initialize provider {provider_class.__name__}: {e}")
|
|
312
|
+
|
|
313
|
+
def _fuzzy_resolve_provider_and_model(
|
|
314
|
+
self, model: str
|
|
315
|
+
) -> Optional[Tuple[Type[OpenAICompatibleProvider], str]]:
|
|
316
|
+
"""
|
|
317
|
+
Performs fuzzy matching to find the closest model match across all providers.
|
|
318
|
+
|
|
319
|
+
Attempts three levels of matching:
|
|
320
|
+
1. Exact case-insensitive match
|
|
321
|
+
2. Substring match (model contains query or vice versa)
|
|
322
|
+
3. Fuzzy match using difflib with 50% cutoff
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
model: The model name or partial name to search for.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
A tuple of (provider_class, resolved_model_name) if a match is found,
|
|
329
|
+
or None if no suitable match is found.
|
|
330
|
+
|
|
331
|
+
Note:
|
|
332
|
+
Prints informational messages if client.print_provider_info is enabled.
|
|
333
|
+
|
|
334
|
+
Examples:
|
|
335
|
+
>>> result = completions._fuzzy_resolve_provider_and_model("gpt-4")
|
|
336
|
+
>>> if result:
|
|
337
|
+
... provider_cls, model_name = result
|
|
338
|
+
... print(f"Found: {model_name} via {provider_cls.__name__}")
|
|
339
|
+
"""
|
|
340
|
+
available = self._get_available_providers()
|
|
341
|
+
model_to_provider = {}
|
|
342
|
+
|
|
343
|
+
for p_name, p_cls in available:
|
|
344
|
+
p_models = _get_models_safely(p_cls, self._client)
|
|
345
|
+
for m in p_models:
|
|
346
|
+
if m not in model_to_provider:
|
|
347
|
+
model_to_provider[m] = p_cls
|
|
348
|
+
|
|
349
|
+
if not model_to_provider:
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
# 1. Exact case-insensitive match
|
|
353
|
+
for m_name in model_to_provider:
|
|
354
|
+
if m_name.lower() == model.lower():
|
|
355
|
+
return model_to_provider[m_name], m_name
|
|
356
|
+
|
|
357
|
+
# 2. Substring match
|
|
358
|
+
for m_name in model_to_provider:
|
|
359
|
+
if model.lower() in m_name.lower() or m_name.lower() in model.lower():
|
|
360
|
+
if self._client.print_provider_info:
|
|
361
|
+
print(f"\033[1;33mSubstring match: '{model}' -> '{m_name}'\033[0m")
|
|
362
|
+
return model_to_provider[m_name], m_name
|
|
363
|
+
|
|
364
|
+
# 3. Fuzzy match with difflib
|
|
365
|
+
matches = difflib.get_close_matches(model, model_to_provider.keys(), n=1, cutoff=0.5)
|
|
366
|
+
if matches:
|
|
367
|
+
matched_model = matches[0]
|
|
368
|
+
if self._client.print_provider_info:
|
|
369
|
+
print(f"\033[1;33mFuzzy match: '{model}' -> '{matched_model}'\033[0m")
|
|
370
|
+
return model_to_provider[matched_model], matched_model
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
def _resolve_provider_and_model(
|
|
374
|
+
self, model: str, provider: Optional[Type[OpenAICompatibleProvider]]
|
|
375
|
+
) -> Tuple[Type[OpenAICompatibleProvider], str]:
|
|
376
|
+
"""
|
|
377
|
+
Resolves the best provider and model name based on input specifications.
|
|
378
|
+
|
|
379
|
+
Handles multiple input formats:
|
|
380
|
+
- "provider/model" format: Parses and resolves to exact provider
|
|
381
|
+
- "auto": Randomly selects an available provider and model
|
|
382
|
+
- Named model: Searches across all providers for exact or fuzzy match
|
|
383
|
+
|
|
384
|
+
Resolution strategy:
|
|
385
|
+
1. If "provider/model" format, find provider by name
|
|
386
|
+
2. If provider specified, use it with given or auto-selected model
|
|
387
|
+
3. If "auto", randomly select from available providers and models
|
|
388
|
+
4. Otherwise, search across providers for exact match
|
|
389
|
+
5. Fall back to fuzzy matching
|
|
390
|
+
6. Finally, randomly select from available providers
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
model: Model identifier. Can be "auto", "provider/model", or model name.
|
|
394
|
+
provider: Optional provider class to constrain resolution.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
A tuple of (provider_class, resolved_model_name).
|
|
398
|
+
|
|
399
|
+
Raises:
|
|
400
|
+
RuntimeError: If no providers are available or model cannot be resolved.
|
|
401
|
+
|
|
402
|
+
Examples:
|
|
403
|
+
>>> # Auto resolution
|
|
404
|
+
>>> p_cls, m_name = completions._resolve_provider_and_model("auto", None)
|
|
405
|
+
>>> # Explicit provider/model
|
|
406
|
+
>>> p_cls, m_name = completions._resolve_provider_and_model(
|
|
407
|
+
... "GPT4Free/gpt-3.5-turbo", None
|
|
408
|
+
... )
|
|
409
|
+
>>> # Model name fuzzy matching
|
|
410
|
+
>>> p_cls, m_name = completions._resolve_provider_and_model("gpt-4", None)
|
|
411
|
+
"""
|
|
412
|
+
if "/" in model:
|
|
413
|
+
p_name, m_name = model.split("/", 1)
|
|
414
|
+
found_p = next(
|
|
415
|
+
(cls for name, cls in OPENAI_PROVIDERS.items() if name.lower() == p_name.lower()),
|
|
416
|
+
None,
|
|
417
|
+
)
|
|
418
|
+
if found_p:
|
|
419
|
+
return found_p, m_name
|
|
420
|
+
|
|
421
|
+
if provider:
|
|
422
|
+
resolved_model = model
|
|
423
|
+
if model == "auto":
|
|
424
|
+
p_models = _get_models_safely(provider, self._client)
|
|
425
|
+
if p_models:
|
|
426
|
+
resolved_model = random.choice(p_models)
|
|
427
|
+
else:
|
|
428
|
+
raise RuntimeError(f"Provider {provider.__name__} has no available models.")
|
|
429
|
+
return provider, resolved_model
|
|
430
|
+
|
|
431
|
+
if model == "auto":
|
|
432
|
+
available = self._get_available_providers()
|
|
433
|
+
if not available:
|
|
434
|
+
raise RuntimeError("No available chat providers found.")
|
|
435
|
+
|
|
436
|
+
providers_with_models = []
|
|
437
|
+
for name, cls in available:
|
|
438
|
+
p_models = _get_models_safely(cls, self._client)
|
|
439
|
+
if p_models:
|
|
440
|
+
providers_with_models.append((cls, p_models))
|
|
441
|
+
|
|
442
|
+
if providers_with_models:
|
|
443
|
+
p_cls, p_models = random.choice(providers_with_models)
|
|
444
|
+
m_name = random.choice(p_models)
|
|
445
|
+
return p_cls, m_name
|
|
446
|
+
else:
|
|
447
|
+
raise RuntimeError("No available chat providers with models found.")
|
|
448
|
+
|
|
449
|
+
available = self._get_available_providers()
|
|
450
|
+
for p_name, p_cls in available:
|
|
451
|
+
p_models = _get_models_safely(p_cls, self._client)
|
|
452
|
+
if p_models and model in p_models:
|
|
453
|
+
return p_cls, model
|
|
454
|
+
|
|
455
|
+
fuzzy_result = self._fuzzy_resolve_provider_and_model(model)
|
|
456
|
+
if fuzzy_result:
|
|
457
|
+
return fuzzy_result
|
|
458
|
+
|
|
459
|
+
if available:
|
|
460
|
+
random.shuffle(available)
|
|
461
|
+
return available[0][1], model
|
|
462
|
+
|
|
463
|
+
raise RuntimeError(f"No providers found for model '{model}'")
|
|
464
|
+
|
|
465
|
+
def _get_available_providers(self) -> List[Tuple[str, Type[OpenAICompatibleProvider]]]:
|
|
466
|
+
"""
|
|
467
|
+
Returns a list of available chat providers for the current client configuration.
|
|
468
|
+
|
|
469
|
+
Filters the global provider registry based on:
|
|
470
|
+
- Client's exclude list
|
|
471
|
+
- API key availability (if api_key is set, includes auth-required providers)
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
A list of tuples containing (provider_name, provider_class) pairs
|
|
475
|
+
for all available providers.
|
|
476
|
+
|
|
477
|
+
Examples:
|
|
478
|
+
>>> providers = completions._get_available_providers()
|
|
479
|
+
>>> print([p[0] for p in providers])
|
|
480
|
+
['GPT4Free', 'OpenRouter', 'Groq']
|
|
481
|
+
"""
|
|
482
|
+
exclude = set(self._client.exclude or [])
|
|
483
|
+
if self._client.api_key:
|
|
484
|
+
return [(name, cls) for name, cls in OPENAI_PROVIDERS.items() if name not in exclude]
|
|
485
|
+
return [
|
|
486
|
+
(name, cls)
|
|
487
|
+
for name, cls in OPENAI_PROVIDERS.items()
|
|
488
|
+
if name not in OPENAI_AUTH_REQUIRED and name not in exclude
|
|
489
|
+
]
|
|
490
|
+
|
|
491
|
+
def create(
|
|
492
|
+
self,
|
|
493
|
+
*,
|
|
494
|
+
model: str = "auto",
|
|
495
|
+
messages: List[Dict[str, Any]],
|
|
496
|
+
max_tokens: Optional[int] = None,
|
|
497
|
+
stream: bool = False,
|
|
498
|
+
temperature: Optional[float] = None,
|
|
499
|
+
top_p: Optional[float] = None,
|
|
500
|
+
tools: Optional[List[Union[Tool, Dict[str, Any]]]] = None,
|
|
501
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
502
|
+
timeout: Optional[int] = None,
|
|
503
|
+
proxies: Optional[dict] = None,
|
|
504
|
+
provider: Optional[Type[OpenAICompatibleProvider]] = None,
|
|
505
|
+
**kwargs: Any,
|
|
506
|
+
) -> Union[ChatCompletion, Generator[ChatCompletionChunk, None, None]]:
|
|
507
|
+
"""
|
|
508
|
+
Creates a chat completion with automatic provider selection and failover.
|
|
509
|
+
|
|
510
|
+
Attempts to resolve the specified model to a provider and model name,
|
|
511
|
+
then creates a completion. If the initial attempt fails, automatically
|
|
512
|
+
falls back to other available providers, prioritizing:
|
|
513
|
+
1. Providers with exact model matches
|
|
514
|
+
2. Providers with fuzzy model matches
|
|
515
|
+
3. Providers with any available model
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
model: Model identifier. Default "auto" randomly selects available models.
|
|
519
|
+
Can be "provider/model" format or model name. Required.
|
|
520
|
+
messages: List of message dicts with 'role' and 'content' keys. Required.
|
|
521
|
+
max_tokens: Maximum tokens in the response. Optional.
|
|
522
|
+
stream: Whether to stream the response. Default is False.
|
|
523
|
+
temperature: Sampling temperature (0-2). Controls response randomness. Optional.
|
|
524
|
+
top_p: Nucleus sampling parameter (0-1). Optional.
|
|
525
|
+
tools: List of tools or tool definitions for function calling. Optional.
|
|
526
|
+
tool_choice: Which tool to use or how to select tools. Optional.
|
|
527
|
+
timeout: Request timeout in seconds. Optional.
|
|
528
|
+
proxies: HTTP proxy configuration dict. Optional.
|
|
529
|
+
provider: Specific provider class to use. Optional.
|
|
530
|
+
**kwargs: Additional arguments passed to the provider.
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
ChatCompletion object for non-streaming requests.
|
|
534
|
+
Generator[ChatCompletionChunk, None, None] for streaming requests.
|
|
535
|
+
|
|
536
|
+
Raises:
|
|
537
|
+
RuntimeError: If all chat providers fail or no providers are available.
|
|
538
|
+
|
|
539
|
+
Note:
|
|
540
|
+
If print_provider_info is True, provider name and model are printed
|
|
541
|
+
to stdout in color-formatted text. Streaming responses print on first chunk.
|
|
542
|
+
|
|
543
|
+
Examples:
|
|
544
|
+
>>> client = Client(print_provider_info=True)
|
|
545
|
+
>>> response = client.chat.completions.create(
|
|
546
|
+
... model="gpt-4",
|
|
547
|
+
... messages=[{"role": "user", "content": "Hello!"}]
|
|
548
|
+
... )
|
|
549
|
+
>>> print(response.choices[0].message.content)
|
|
550
|
+
|
|
551
|
+
>>> # Streaming example
|
|
552
|
+
>>> for chunk in client.chat.completions.create(
|
|
553
|
+
... model="auto",
|
|
554
|
+
... messages=[{"role": "user", "content": "Hello!"}],
|
|
555
|
+
... stream=True
|
|
556
|
+
... ):
|
|
557
|
+
... print(chunk.choices[0].delta.content, end="")
|
|
558
|
+
"""
|
|
559
|
+
try:
|
|
560
|
+
resolved_provider, resolved_model = self._resolve_provider_and_model(model, provider)
|
|
561
|
+
except Exception:
|
|
562
|
+
resolved_provider, resolved_model = None, model
|
|
563
|
+
|
|
564
|
+
call_kwargs = {
|
|
565
|
+
"model": resolved_model,
|
|
566
|
+
"messages": messages,
|
|
567
|
+
"stream": stream,
|
|
568
|
+
}
|
|
569
|
+
if max_tokens is not None:
|
|
570
|
+
call_kwargs["max_tokens"] = max_tokens
|
|
571
|
+
if temperature is not None:
|
|
572
|
+
call_kwargs["temperature"] = temperature
|
|
573
|
+
if top_p is not None:
|
|
574
|
+
call_kwargs["top_p"] = top_p
|
|
575
|
+
if tools is not None:
|
|
576
|
+
call_kwargs["tools"] = tools
|
|
577
|
+
if tool_choice is not None:
|
|
578
|
+
call_kwargs["tool_choice"] = tool_choice
|
|
579
|
+
if timeout is not None:
|
|
580
|
+
call_kwargs["timeout"] = timeout
|
|
581
|
+
if proxies is not None:
|
|
582
|
+
call_kwargs["proxies"] = proxies
|
|
583
|
+
call_kwargs.update(kwargs)
|
|
584
|
+
|
|
585
|
+
if resolved_provider:
|
|
586
|
+
try:
|
|
587
|
+
provider_instance = self._get_provider_instance(resolved_provider)
|
|
588
|
+
response = provider_instance.chat.completions.create(
|
|
589
|
+
**cast(Dict[str, Any], call_kwargs)
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
if stream and inspect.isgenerator(response):
|
|
593
|
+
try:
|
|
594
|
+
first_chunk = next(response)
|
|
595
|
+
self._last_provider = resolved_provider.__name__
|
|
596
|
+
|
|
597
|
+
def _chained_gen_stream(
|
|
598
|
+
first: ChatCompletionChunk,
|
|
599
|
+
rest: Generator[ChatCompletionChunk, None, None],
|
|
600
|
+
pname: str,
|
|
601
|
+
) -> Generator[ChatCompletionChunk, None, None]:
|
|
602
|
+
if self._client.print_provider_info:
|
|
603
|
+
print(f"\033[1;34m{pname}:{resolved_model}\033[0m\n")
|
|
604
|
+
yield first
|
|
605
|
+
yield from rest
|
|
606
|
+
|
|
607
|
+
return _chained_gen_stream(
|
|
608
|
+
first_chunk, response, resolved_provider.__name__
|
|
609
|
+
)
|
|
610
|
+
except StopIteration:
|
|
611
|
+
pass
|
|
612
|
+
except Exception:
|
|
613
|
+
pass
|
|
614
|
+
else:
|
|
615
|
+
# Type narrowing for non-streaming response
|
|
616
|
+
if not inspect.isgenerator(response):
|
|
617
|
+
completion_response = cast(ChatCompletion, response)
|
|
618
|
+
if (
|
|
619
|
+
completion_response
|
|
620
|
+
and hasattr(completion_response, "choices")
|
|
621
|
+
and completion_response.choices
|
|
622
|
+
and completion_response.choices[0].message
|
|
623
|
+
and completion_response.choices[0].message.content
|
|
624
|
+
and completion_response.choices[0].message.content.strip()
|
|
625
|
+
):
|
|
626
|
+
self._last_provider = resolved_provider.__name__
|
|
627
|
+
if self._client.print_provider_info:
|
|
628
|
+
print(
|
|
629
|
+
f"\033[1;34m{resolved_provider.__name__}:{resolved_model}\033[0m\n"
|
|
630
|
+
)
|
|
631
|
+
return completion_response
|
|
632
|
+
else:
|
|
633
|
+
raise ValueError(
|
|
634
|
+
f"Provider {resolved_provider.__name__} returned empty content"
|
|
635
|
+
)
|
|
636
|
+
except Exception:
|
|
637
|
+
pass
|
|
638
|
+
|
|
639
|
+
all_available = self._get_available_providers()
|
|
640
|
+
tier1, tier2, tier3 = [], [], []
|
|
641
|
+
base_model = model.split("/")[-1] if "/" in model else model
|
|
642
|
+
search_models = {base_model, resolved_model} if resolved_model else {base_model}
|
|
643
|
+
|
|
644
|
+
for p_name, p_cls in all_available:
|
|
645
|
+
if p_cls == resolved_provider:
|
|
646
|
+
continue
|
|
647
|
+
|
|
648
|
+
p_models = _get_models_safely(p_cls, self._client)
|
|
649
|
+
if not p_models:
|
|
650
|
+
fallback_model = (
|
|
651
|
+
base_model
|
|
652
|
+
if base_model != "auto"
|
|
653
|
+
else (p_models[0] if p_models else base_model)
|
|
654
|
+
)
|
|
655
|
+
tier3.append((p_name, p_cls, fallback_model))
|
|
656
|
+
continue
|
|
657
|
+
|
|
658
|
+
found_exact = False
|
|
659
|
+
for sm in search_models:
|
|
660
|
+
if sm != "auto" and sm in p_models:
|
|
661
|
+
tier1.append((p_name, p_cls, sm))
|
|
662
|
+
found_exact = True
|
|
663
|
+
break
|
|
664
|
+
if found_exact:
|
|
665
|
+
continue
|
|
666
|
+
|
|
667
|
+
if base_model != "auto":
|
|
668
|
+
matches = difflib.get_close_matches(base_model, p_models, n=1, cutoff=0.5)
|
|
669
|
+
if matches:
|
|
670
|
+
tier2.append((p_name, p_cls, matches[0]))
|
|
671
|
+
continue
|
|
672
|
+
|
|
673
|
+
tier3.append((p_name, p_cls, random.choice(p_models)))
|
|
674
|
+
|
|
675
|
+
random.shuffle(tier1)
|
|
676
|
+
random.shuffle(tier2)
|
|
677
|
+
random.shuffle(tier3)
|
|
678
|
+
fallback_queue = tier1 + tier2 + tier3
|
|
679
|
+
|
|
680
|
+
errors = []
|
|
681
|
+
for p_name, p_cls, p_model in fallback_queue:
|
|
682
|
+
try:
|
|
683
|
+
provider_instance = self._get_provider_instance(p_cls)
|
|
684
|
+
fallback_kwargs = cast(
|
|
685
|
+
Dict[str, Any], {**call_kwargs, "model": p_model}
|
|
686
|
+
)
|
|
687
|
+
response = provider_instance.chat.completions.create(**fallback_kwargs)
|
|
688
|
+
|
|
689
|
+
if stream and inspect.isgenerator(response):
|
|
690
|
+
try:
|
|
691
|
+
first_chunk = next(response)
|
|
692
|
+
self._last_provider = p_name
|
|
693
|
+
|
|
694
|
+
def _chained_gen_fallback(first, rest, pname, mname):
|
|
695
|
+
if self._client.print_provider_info:
|
|
696
|
+
print(f"\033[1;34m{pname}:{mname} (Fallback)\033[0m\n")
|
|
697
|
+
yield first
|
|
698
|
+
yield from rest
|
|
699
|
+
|
|
700
|
+
return _chained_gen_fallback(first_chunk, response, p_name, p_model)
|
|
701
|
+
except (StopIteration, Exception):
|
|
702
|
+
continue
|
|
703
|
+
|
|
704
|
+
if not inspect.isgenerator(response):
|
|
705
|
+
completion_response = cast(ChatCompletion, response)
|
|
706
|
+
if (
|
|
707
|
+
completion_response
|
|
708
|
+
and hasattr(completion_response, "choices")
|
|
709
|
+
and completion_response.choices
|
|
710
|
+
and completion_response.choices[0].message
|
|
711
|
+
and completion_response.choices[0].message.content
|
|
712
|
+
and completion_response.choices[0].message.content.strip()
|
|
713
|
+
):
|
|
714
|
+
self._last_provider = p_name
|
|
715
|
+
if self._client.print_provider_info:
|
|
716
|
+
print(f"\033[1;34m{p_name}:{p_model} (Fallback)\033[0m\n")
|
|
717
|
+
return completion_response
|
|
718
|
+
else:
|
|
719
|
+
errors.append(f"{p_name}: Returned empty response.")
|
|
720
|
+
continue
|
|
721
|
+
except Exception as e:
|
|
722
|
+
errors.append(f"{p_name}: {str(e)}")
|
|
723
|
+
continue
|
|
724
|
+
|
|
725
|
+
raise RuntimeError(f"All chat providers failed. Errors: {'; '.join(errors[:3])}")
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
class ClientChat(BaseChat):
|
|
729
|
+
"""
|
|
730
|
+
Standard chat interface wrapper for the Client.
|
|
731
|
+
|
|
732
|
+
Provides access to chat completions through a completions property that
|
|
733
|
+
implements the BaseChat interface. Acts as an adapter between the Client
|
|
734
|
+
and the underlying OpenAI-compatible completion system.
|
|
735
|
+
|
|
736
|
+
Attributes:
|
|
737
|
+
completions: ClientCompletions instance for creating chat completions.
|
|
738
|
+
|
|
739
|
+
Examples:
|
|
740
|
+
>>> chat = client.chat
|
|
741
|
+
>>> response = chat.completions.create(
|
|
742
|
+
... model="auto",
|
|
743
|
+
... messages=[{"role": "user", "content": "Hi"}]
|
|
744
|
+
... )
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
def __init__(self, client: "Client"):
|
|
748
|
+
self.completions = ClientCompletions(client)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
class ClientImages(BaseImages):
|
|
752
|
+
"""
|
|
753
|
+
Unified image generation interface with automatic provider selection and failover.
|
|
754
|
+
|
|
755
|
+
Manages text-to-image (TTI) generation by automatically selecting appropriate
|
|
756
|
+
providers and models based on user input. Implements similar resolution and
|
|
757
|
+
failover logic as ClientCompletions but for image generation.
|
|
758
|
+
|
|
759
|
+
Features:
|
|
760
|
+
- Automatic model discovery and fuzzy matching
|
|
761
|
+
- Provider failover for reliability
|
|
762
|
+
- Provider and model caching for performance
|
|
763
|
+
- Structured parameter validation
|
|
764
|
+
- Support for multiple image output formats
|
|
765
|
+
|
|
766
|
+
Attributes:
|
|
767
|
+
_client: Reference to the parent Client instance.
|
|
768
|
+
_last_provider: Name of the last successfully used image provider.
|
|
769
|
+
|
|
770
|
+
Examples:
|
|
771
|
+
>>> client = Client(print_provider_info=True)
|
|
772
|
+
>>> response = client.images.generate(
|
|
773
|
+
... prompt="A beautiful sunset",
|
|
774
|
+
... model="auto",
|
|
775
|
+
... n=1,
|
|
776
|
+
... size="1024x1024"
|
|
777
|
+
... )
|
|
778
|
+
>>> print(response.data[0].url)
|
|
779
|
+
"""
|
|
780
|
+
|
|
781
|
+
def __init__(self, client: "Client"):
|
|
782
|
+
self._client = client
|
|
783
|
+
self._last_provider: Optional[str] = None
|
|
784
|
+
|
|
785
|
+
@property
|
|
786
|
+
def last_provider(self) -> Optional[str]:
|
|
787
|
+
"""
|
|
788
|
+
Returns the name of the last successfully used image provider.
|
|
789
|
+
|
|
790
|
+
Tracks which TTI provider was most recently used to generate images.
|
|
791
|
+
Useful for debugging and understanding which fallback providers are
|
|
792
|
+
being utilized.
|
|
793
|
+
|
|
794
|
+
Returns:
|
|
795
|
+
The name of the last provider as a string, or None if no provider
|
|
796
|
+
has been successfully used yet.
|
|
797
|
+
|
|
798
|
+
Examples:
|
|
799
|
+
>>> images = client.images
|
|
800
|
+
>>> response = images.generate(prompt="...", model="auto")
|
|
801
|
+
>>> print(images.last_provider)
|
|
802
|
+
'StableDiffusion'
|
|
803
|
+
"""
|
|
804
|
+
return self._last_provider
|
|
805
|
+
|
|
806
|
+
def _get_provider_instance(
|
|
807
|
+
self, provider_class: Type[TTICompatibleProvider], **kwargs
|
|
808
|
+
) -> TTICompatibleProvider:
|
|
809
|
+
"""
|
|
810
|
+
Retrieves or creates a cached TTI provider instance.
|
|
811
|
+
|
|
812
|
+
Checks if a TTI provider instance already exists in the client's cache.
|
|
813
|
+
If not, initializes a new instance with client-level configuration
|
|
814
|
+
(proxies) merged with any additional kwargs.
|
|
815
|
+
|
|
816
|
+
Args:
|
|
817
|
+
provider_class: The TTI-compatible provider class to instantiate.
|
|
818
|
+
**kwargs: Additional keyword arguments to pass to the provider's constructor.
|
|
819
|
+
|
|
820
|
+
Returns:
|
|
821
|
+
An instantiated and initialized TTI provider instance.
|
|
822
|
+
|
|
823
|
+
Raises:
|
|
824
|
+
RuntimeError: If the provider cannot be initialized with or without
|
|
825
|
+
client configuration.
|
|
826
|
+
|
|
827
|
+
Examples:
|
|
828
|
+
>>> from webscout.Provider.TTI.dalle import DALLE
|
|
829
|
+
>>> images = client.images
|
|
830
|
+
>>> instance = images._get_provider_instance(DALLE)
|
|
831
|
+
"""
|
|
832
|
+
p_name = provider_class.__name__
|
|
833
|
+
if p_name in self._client._provider_cache:
|
|
834
|
+
return self._client._provider_cache[p_name]
|
|
835
|
+
|
|
836
|
+
init_kwargs = {}
|
|
837
|
+
if self._client.proxies:
|
|
838
|
+
init_kwargs["proxies"] = self._client.proxies
|
|
839
|
+
init_kwargs.update(kwargs)
|
|
840
|
+
|
|
841
|
+
try:
|
|
842
|
+
instance = provider_class(**init_kwargs)
|
|
843
|
+
self._client._provider_cache[p_name] = instance
|
|
844
|
+
return instance
|
|
845
|
+
except Exception:
|
|
846
|
+
try:
|
|
847
|
+
instance = provider_class()
|
|
848
|
+
self._client._provider_cache[p_name] = instance
|
|
849
|
+
return instance
|
|
850
|
+
except Exception as e:
|
|
851
|
+
raise RuntimeError(
|
|
852
|
+
f"Failed to initialize TTI provider {provider_class.__name__}: {e}"
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
def _fuzzy_resolve_provider_and_model(
|
|
856
|
+
self, model: str
|
|
857
|
+
) -> Optional[Tuple[Type[TTICompatibleProvider], str]]:
|
|
858
|
+
"""
|
|
859
|
+
Performs fuzzy matching to find the closest image model match across providers.
|
|
860
|
+
|
|
861
|
+
Attempts three levels of matching:
|
|
862
|
+
1. Exact case-insensitive match
|
|
863
|
+
2. Substring match (model contains query or vice versa)
|
|
864
|
+
3. Fuzzy match using difflib with 50% cutoff
|
|
865
|
+
|
|
866
|
+
Args:
|
|
867
|
+
model: The model name or partial name to search for.
|
|
868
|
+
|
|
869
|
+
Returns:
|
|
870
|
+
A tuple of (provider_class, resolved_model_name) if a match is found,
|
|
871
|
+
or None if no suitable match is found.
|
|
872
|
+
|
|
873
|
+
Note:
|
|
874
|
+
Prints informational messages if client.print_provider_info is enabled.
|
|
875
|
+
|
|
876
|
+
Examples:
|
|
877
|
+
>>> result = images._fuzzy_resolve_provider_and_model("dall-e")
|
|
878
|
+
>>> if result:
|
|
879
|
+
... provider_cls, model_name = result
|
|
880
|
+
... print(f"Found: {model_name} via {provider_cls.__name__}")
|
|
881
|
+
"""
|
|
882
|
+
available = self._get_available_providers()
|
|
883
|
+
model_to_provider = {}
|
|
884
|
+
|
|
885
|
+
for p_name, p_cls in available:
|
|
886
|
+
p_models = _get_models_safely(p_cls, self._client)
|
|
887
|
+
for m in p_models:
|
|
888
|
+
if m not in model_to_provider:
|
|
889
|
+
model_to_provider[m] = p_cls
|
|
890
|
+
|
|
891
|
+
if not model_to_provider:
|
|
892
|
+
return None
|
|
893
|
+
|
|
894
|
+
# 1. Exact match
|
|
895
|
+
for m_name in model_to_provider:
|
|
896
|
+
if m_name.lower() == model.lower():
|
|
897
|
+
return model_to_provider[m_name], m_name
|
|
898
|
+
|
|
899
|
+
# 2. Substring match
|
|
900
|
+
for m_name in model_to_provider:
|
|
901
|
+
if model.lower() in m_name.lower() or m_name.lower() in model.lower():
|
|
902
|
+
if self._client.print_provider_info:
|
|
903
|
+
print(f"\033[1;33mSubstring match (TTI): '{model}' -> '{m_name}'\033[0m")
|
|
904
|
+
return model_to_provider[m_name], m_name
|
|
905
|
+
|
|
906
|
+
# 3. Fuzzy match
|
|
907
|
+
matches = difflib.get_close_matches(model, model_to_provider.keys(), n=1, cutoff=0.5)
|
|
908
|
+
if matches:
|
|
909
|
+
matched_model = matches[0]
|
|
910
|
+
if self._client.print_provider_info:
|
|
911
|
+
print(f"\033[1;33mFuzzy match (TTI): '{model}' -> '{matched_model}'\033[0m")
|
|
912
|
+
return model_to_provider[matched_model], matched_model
|
|
913
|
+
return None
|
|
914
|
+
|
|
915
|
+
def _resolve_provider_and_model(
|
|
916
|
+
self, model: str, provider: Optional[Type[TTICompatibleProvider]]
|
|
917
|
+
) -> Tuple[Type[TTICompatibleProvider], str]:
|
|
918
|
+
"""
|
|
919
|
+
Resolves the best TTI provider and model name based on input specifications.
|
|
920
|
+
|
|
921
|
+
Handles multiple input formats:
|
|
922
|
+
- "provider/model" format: Parses and resolves to exact provider
|
|
923
|
+
- "auto": Randomly selects an available provider and model
|
|
924
|
+
- Named model: Searches across all providers for exact or fuzzy match
|
|
925
|
+
|
|
926
|
+
Resolution strategy:
|
|
927
|
+
1. If "provider/model" format, find provider by name
|
|
928
|
+
2. If provider specified, use it with given or auto-selected model
|
|
929
|
+
3. If "auto", randomly select from available providers and models
|
|
930
|
+
4. Otherwise, search across providers for exact match
|
|
931
|
+
5. Fall back to fuzzy matching
|
|
932
|
+
6. Finally, randomly select from available providers
|
|
933
|
+
|
|
934
|
+
Args:
|
|
935
|
+
model: Model identifier. Can be "auto", "provider/model", or model name.
|
|
936
|
+
provider: Optional TTI provider class to constrain resolution.
|
|
937
|
+
|
|
938
|
+
Returns:
|
|
939
|
+
A tuple of (provider_class, resolved_model_name).
|
|
940
|
+
|
|
941
|
+
Raises:
|
|
942
|
+
RuntimeError: If no providers are available or model cannot be resolved.
|
|
943
|
+
|
|
944
|
+
Examples:
|
|
945
|
+
>>> # Auto resolution
|
|
946
|
+
>>> p_cls, m_name = images._resolve_provider_and_model("auto", None)
|
|
947
|
+
>>> # Explicit provider/model
|
|
948
|
+
>>> p_cls, m_name = images._resolve_provider_and_model(
|
|
949
|
+
... "StableDiffusion/stable-diffusion-v1-5", None
|
|
950
|
+
... )
|
|
951
|
+
"""
|
|
952
|
+
if "/" in model:
|
|
953
|
+
p_name, m_name = model.split("/", 1)
|
|
954
|
+
found_p = next(
|
|
955
|
+
(cls for name, cls in TTI_PROVIDERS.items() if name.lower() == p_name.lower()), None
|
|
956
|
+
)
|
|
957
|
+
if found_p:
|
|
958
|
+
return found_p, m_name
|
|
959
|
+
|
|
960
|
+
if provider:
|
|
961
|
+
resolved_model = model
|
|
962
|
+
if model == "auto":
|
|
963
|
+
p_models = _get_models_safely(provider, self._client)
|
|
964
|
+
if p_models:
|
|
965
|
+
resolved_model = random.choice(p_models)
|
|
966
|
+
else:
|
|
967
|
+
raise RuntimeError(f"TTI Provider {provider.__name__} has no available models.")
|
|
968
|
+
return provider, resolved_model
|
|
969
|
+
|
|
970
|
+
if model == "auto":
|
|
971
|
+
available = self._get_available_providers()
|
|
972
|
+
if not available:
|
|
973
|
+
raise RuntimeError("No available image providers found.")
|
|
974
|
+
|
|
975
|
+
providers_with_models = []
|
|
976
|
+
for name, cls in available:
|
|
977
|
+
p_models = _get_models_safely(cls, self._client)
|
|
978
|
+
if p_models:
|
|
979
|
+
providers_with_models.append((cls, p_models))
|
|
980
|
+
|
|
981
|
+
if providers_with_models:
|
|
982
|
+
p_cls, p_models = random.choice(providers_with_models)
|
|
983
|
+
return p_cls, random.choice(p_models)
|
|
984
|
+
else:
|
|
985
|
+
raise RuntimeError("No available image providers with models found.")
|
|
986
|
+
|
|
987
|
+
available = self._get_available_providers()
|
|
988
|
+
for p_name, p_cls in available:
|
|
989
|
+
p_models = _get_models_safely(p_cls, self._client)
|
|
990
|
+
if p_models and model in p_models:
|
|
991
|
+
return p_cls, model
|
|
992
|
+
|
|
993
|
+
fuzzy_result = self._fuzzy_resolve_provider_and_model(model)
|
|
994
|
+
if fuzzy_result:
|
|
995
|
+
return fuzzy_result
|
|
996
|
+
|
|
997
|
+
if available:
|
|
998
|
+
random.shuffle(available)
|
|
999
|
+
return available[0][1], model
|
|
1000
|
+
raise RuntimeError(f"No image providers found for model '{model}'")
|
|
1001
|
+
|
|
1002
|
+
def _get_available_providers(self) -> List[Tuple[str, Type[TTICompatibleProvider]]]:
|
|
1003
|
+
"""
|
|
1004
|
+
Returns a list of available image providers for the current client configuration.
|
|
1005
|
+
|
|
1006
|
+
Filters the global TTI provider registry based on:
|
|
1007
|
+
- Client's exclude_images list
|
|
1008
|
+
- API key availability (if api_key is set, includes auth-required providers)
|
|
1009
|
+
|
|
1010
|
+
Returns:
|
|
1011
|
+
A list of tuples containing (provider_name, provider_class) pairs
|
|
1012
|
+
for all available image providers.
|
|
1013
|
+
|
|
1014
|
+
Examples:
|
|
1015
|
+
>>> providers = images._get_available_providers()
|
|
1016
|
+
>>> print([p[0] for p in providers])
|
|
1017
|
+
['StableDiffusion', 'DALL-E', 'Midjourney']
|
|
1018
|
+
"""
|
|
1019
|
+
exclude = set(self._client.exclude_images or [])
|
|
1020
|
+
if self._client.api_key:
|
|
1021
|
+
return [(name, cls) for name, cls in TTI_PROVIDERS.items() if name not in exclude]
|
|
1022
|
+
return [
|
|
1023
|
+
(name, cls)
|
|
1024
|
+
for name, cls in TTI_PROVIDERS.items()
|
|
1025
|
+
if name not in TTI_AUTH_REQUIRED and name not in exclude
|
|
1026
|
+
]
|
|
1027
|
+
|
|
1028
|
+
def generate(
|
|
1029
|
+
self,
|
|
1030
|
+
*,
|
|
1031
|
+
prompt: str,
|
|
1032
|
+
model: str = "auto",
|
|
1033
|
+
n: int = 1,
|
|
1034
|
+
size: str = "1024x1024",
|
|
1035
|
+
response_format: str = "url",
|
|
1036
|
+
provider: Optional[Type[TTICompatibleProvider]] = None,
|
|
1037
|
+
**kwargs: Any,
|
|
1038
|
+
) -> ImageResponse:
|
|
1039
|
+
"""
|
|
1040
|
+
Generates images with automatic provider selection and failover.
|
|
1041
|
+
|
|
1042
|
+
Attempts to resolve the specified model to a provider and model name,
|
|
1043
|
+
then creates images. If the initial attempt fails, automatically falls
|
|
1044
|
+
back to other available providers, prioritizing:
|
|
1045
|
+
1. Providers with exact model matches
|
|
1046
|
+
2. Providers with fuzzy model matches
|
|
1047
|
+
3. Providers with any available model
|
|
1048
|
+
|
|
1049
|
+
Args:
|
|
1050
|
+
prompt: Text description of the image(s) to generate. Required.
|
|
1051
|
+
model: Model identifier. Default "auto" randomly selects available models.
|
|
1052
|
+
Can be "provider/model" format or model name.
|
|
1053
|
+
n: Number of images to generate. Default is 1.
|
|
1054
|
+
size: Image size specification (e.g., "1024x1024", "512x512"). Default is "1024x1024".
|
|
1055
|
+
response_format: Format for image response ("url" or "b64_json"). Default is "url".
|
|
1056
|
+
provider: Specific TTI provider class to use. Optional.
|
|
1057
|
+
**kwargs: Additional arguments passed to the provider.
|
|
1058
|
+
|
|
1059
|
+
Returns:
|
|
1060
|
+
ImageResponse object containing generated images with URLs or base64 data.
|
|
1061
|
+
|
|
1062
|
+
Raises:
|
|
1063
|
+
RuntimeError: If all image providers fail or no providers are available.
|
|
1064
|
+
|
|
1065
|
+
Note:
|
|
1066
|
+
If print_provider_info is True, provider name and model are printed
|
|
1067
|
+
to stdout in color-formatted text.
|
|
1068
|
+
|
|
1069
|
+
Examples:
|
|
1070
|
+
>>> client = Client(print_provider_info=True)
|
|
1071
|
+
>>> response = client.images.generate(
|
|
1072
|
+
... prompt="A beautiful sunset over mountains",
|
|
1073
|
+
... model="auto",
|
|
1074
|
+
... n=1,
|
|
1075
|
+
... size="1024x1024"
|
|
1076
|
+
... )
|
|
1077
|
+
>>> print(response.data[0].url)
|
|
1078
|
+
|
|
1079
|
+
>>> # Using specific provider
|
|
1080
|
+
>>> from webscout.Provider.TTI.stable import StableDiffusion
|
|
1081
|
+
>>> response = client.images.generate(
|
|
1082
|
+
... prompt="A cat wearing sunglasses",
|
|
1083
|
+
... provider=StableDiffusion
|
|
1084
|
+
... )
|
|
1085
|
+
"""
|
|
1086
|
+
try:
|
|
1087
|
+
resolved_provider, resolved_model = self._resolve_provider_and_model(model, provider)
|
|
1088
|
+
except Exception:
|
|
1089
|
+
resolved_provider, resolved_model = None, model
|
|
1090
|
+
|
|
1091
|
+
call_kwargs = {
|
|
1092
|
+
"prompt": prompt,
|
|
1093
|
+
"model": resolved_model,
|
|
1094
|
+
"n": n,
|
|
1095
|
+
"size": size,
|
|
1096
|
+
"response_format": response_format,
|
|
1097
|
+
}
|
|
1098
|
+
call_kwargs.update(kwargs)
|
|
1099
|
+
|
|
1100
|
+
if resolved_provider:
|
|
1101
|
+
try:
|
|
1102
|
+
provider_instance = self._get_provider_instance(resolved_provider)
|
|
1103
|
+
response = provider_instance.images.create(
|
|
1104
|
+
**cast(Dict[str, Any], call_kwargs)
|
|
1105
|
+
)
|
|
1106
|
+
self._last_provider = resolved_provider.__name__
|
|
1107
|
+
if self._client.print_provider_info:
|
|
1108
|
+
print(f"\033[1;34m{resolved_provider.__name__}:{resolved_model}\033[0m\n")
|
|
1109
|
+
return response
|
|
1110
|
+
except Exception:
|
|
1111
|
+
pass
|
|
1112
|
+
|
|
1113
|
+
all_available = self._get_available_providers()
|
|
1114
|
+
tier1, tier2, tier3 = [], [], []
|
|
1115
|
+
base_model = model.split("/")[-1] if "/" in model else model
|
|
1116
|
+
search_models = {base_model, resolved_model} if resolved_model else {base_model}
|
|
1117
|
+
|
|
1118
|
+
for p_name, p_cls in all_available:
|
|
1119
|
+
if p_cls == resolved_provider:
|
|
1120
|
+
continue
|
|
1121
|
+
|
|
1122
|
+
p_models = _get_models_safely(p_cls, self._client)
|
|
1123
|
+
if not p_models:
|
|
1124
|
+
fallback_model = (
|
|
1125
|
+
base_model
|
|
1126
|
+
if base_model != "auto"
|
|
1127
|
+
else (p_models[0] if p_models else base_model)
|
|
1128
|
+
)
|
|
1129
|
+
tier3.append((p_name, p_cls, fallback_model))
|
|
1130
|
+
continue
|
|
1131
|
+
|
|
1132
|
+
found_exact = False
|
|
1133
|
+
for sm in search_models:
|
|
1134
|
+
if sm != "auto" and sm in p_models:
|
|
1135
|
+
tier1.append((p_name, p_cls, sm))
|
|
1136
|
+
found_exact = True
|
|
1137
|
+
break
|
|
1138
|
+
if found_exact:
|
|
1139
|
+
continue
|
|
1140
|
+
|
|
1141
|
+
if base_model != "auto":
|
|
1142
|
+
matches = difflib.get_close_matches(base_model, p_models, n=1, cutoff=0.5)
|
|
1143
|
+
if matches:
|
|
1144
|
+
tier2.append((p_name, p_cls, matches[0]))
|
|
1145
|
+
continue
|
|
1146
|
+
|
|
1147
|
+
tier3.append((p_name, p_cls, random.choice(p_models)))
|
|
1148
|
+
|
|
1149
|
+
random.shuffle(tier1)
|
|
1150
|
+
random.shuffle(tier2)
|
|
1151
|
+
random.shuffle(tier3)
|
|
1152
|
+
fallback_queue = tier1 + tier2 + tier3
|
|
1153
|
+
|
|
1154
|
+
for p_name, p_cls, p_model in fallback_queue:
|
|
1155
|
+
try:
|
|
1156
|
+
provider_instance = self._get_provider_instance(p_cls)
|
|
1157
|
+
fallback_kwargs = cast(
|
|
1158
|
+
Dict[str, Any], {**call_kwargs, "model": p_model}
|
|
1159
|
+
)
|
|
1160
|
+
response = provider_instance.images.create(**fallback_kwargs)
|
|
1161
|
+
self._last_provider = p_name
|
|
1162
|
+
if self._client.print_provider_info:
|
|
1163
|
+
print(f"\033[1;34m{p_name}:{p_model} (Fallback)\033[0m\n")
|
|
1164
|
+
return response
|
|
1165
|
+
except Exception:
|
|
1166
|
+
continue
|
|
1167
|
+
raise RuntimeError("All image providers failed.")
|
|
1168
|
+
|
|
1169
|
+
def create(self, **kwargs) -> ImageResponse:
|
|
1170
|
+
"""
|
|
1171
|
+
Alias for generate() method.
|
|
1172
|
+
|
|
1173
|
+
Provides compatibility with OpenAI-style image API where create() is
|
|
1174
|
+
the standard method name for image generation.
|
|
1175
|
+
|
|
1176
|
+
Args:
|
|
1177
|
+
**kwargs: All arguments accepted by generate().
|
|
1178
|
+
|
|
1179
|
+
Returns:
|
|
1180
|
+
ImageResponse object containing generated images.
|
|
1181
|
+
|
|
1182
|
+
Examples:
|
|
1183
|
+
>>> response = client.images.create(
|
|
1184
|
+
... prompt="A robot painting a picture",
|
|
1185
|
+
... model="auto"
|
|
1186
|
+
... )
|
|
1187
|
+
"""
|
|
1188
|
+
return self.generate(**kwargs)
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
class Client:
|
|
1192
|
+
"""
|
|
1193
|
+
Unified Webscout Client for AI chat and image generation.
|
|
1194
|
+
|
|
1195
|
+
A high-level client that provides a single interface for interacting with
|
|
1196
|
+
multiple AI providers (chat completions and image generation). Automatically
|
|
1197
|
+
selects, caches, and fails over between providers based on availability
|
|
1198
|
+
and model support.
|
|
1199
|
+
|
|
1200
|
+
This client aims to provide a seamless, provider-agnostic experience by:
|
|
1201
|
+
- Supporting automatic provider selection and fallback
|
|
1202
|
+
- Caching provider instances for performance
|
|
1203
|
+
- Offering intelligent model resolution (auto, provider/model, or model name)
|
|
1204
|
+
- Handling authentication across multiple providers
|
|
1205
|
+
- Providing detailed provider information when enabled
|
|
1206
|
+
|
|
1207
|
+
Attributes:
|
|
1208
|
+
provider: Optional default provider for chat completions.
|
|
1209
|
+
image_provider: Optional default provider for image generation.
|
|
1210
|
+
api_key: Optional API key for providers that support authentication.
|
|
1211
|
+
proxies: HTTP proxy configuration dictionary.
|
|
1212
|
+
exclude: List of provider names to exclude from chat completions.
|
|
1213
|
+
exclude_images: List of provider names to exclude from image generation.
|
|
1214
|
+
print_provider_info: Whether to print selected provider and model info.
|
|
1215
|
+
chat: ClientChat instance for chat completions.
|
|
1216
|
+
images: ClientImages instance for image generation.
|
|
1217
|
+
|
|
1218
|
+
Examples:
|
|
1219
|
+
>>> # Basic usage with automatic provider selection
|
|
1220
|
+
>>> client = Client()
|
|
1221
|
+
>>> response = client.chat.completions.create(
|
|
1222
|
+
... model="auto",
|
|
1223
|
+
... messages=[{"role": "user", "content": "Hello!"}]
|
|
1224
|
+
... )
|
|
1225
|
+
>>> print(response.choices[0].message.content)
|
|
1226
|
+
|
|
1227
|
+
>>> # With provider information and image generation
|
|
1228
|
+
>>> client = Client(print_provider_info=True)
|
|
1229
|
+
>>> chat_response = client.chat.completions.create(
|
|
1230
|
+
... model="gpt-4",
|
|
1231
|
+
... messages=[{"role": "user", "content": "Describe an image"}]
|
|
1232
|
+
... )
|
|
1233
|
+
>>> image_response = client.images.generate(
|
|
1234
|
+
... prompt="A sunset over mountains",
|
|
1235
|
+
... model="auto"
|
|
1236
|
+
... )
|
|
1237
|
+
|
|
1238
|
+
>>> # Excluding certain providers and using API key
|
|
1239
|
+
>>> client = Client(
|
|
1240
|
+
... api_key="your-api-key-here",
|
|
1241
|
+
... exclude=["BadProvider"],
|
|
1242
|
+
... exclude_images=["SlowProvider"]
|
|
1243
|
+
... )
|
|
1244
|
+
"""
|
|
1245
|
+
|
|
1246
|
+
def __init__(
|
|
1247
|
+
self,
|
|
1248
|
+
provider: Optional[Type[OpenAICompatibleProvider]] = None,
|
|
1249
|
+
image_provider: Optional[Type[TTICompatibleProvider]] = None,
|
|
1250
|
+
api_key: Optional[str] = None,
|
|
1251
|
+
proxies: Optional[dict] = None,
|
|
1252
|
+
exclude: Optional[List[str]] = None,
|
|
1253
|
+
exclude_images: Optional[List[str]] = None,
|
|
1254
|
+
print_provider_info: bool = False,
|
|
1255
|
+
**kwargs: Any,
|
|
1256
|
+
):
|
|
1257
|
+
"""
|
|
1258
|
+
Initialize the Webscout Client with optional configuration.
|
|
1259
|
+
|
|
1260
|
+
Args:
|
|
1261
|
+
provider: Default provider class for chat completions. If specified,
|
|
1262
|
+
this provider is prioritized in provider resolution. Optional.
|
|
1263
|
+
image_provider: Default provider class for image generation. If specified,
|
|
1264
|
+
this provider is prioritized in image resolution. Optional.
|
|
1265
|
+
api_key: API key for authenticated providers. If provided, enables access
|
|
1266
|
+
to providers that require authentication. Optional.
|
|
1267
|
+
proxies: Dictionary of proxy settings (e.g., {"http": "http://proxy:8080"}).
|
|
1268
|
+
Applied to all provider requests. Optional.
|
|
1269
|
+
exclude: List of provider names to exclude from chat completion selection.
|
|
1270
|
+
Names are case-insensitive. Optional.
|
|
1271
|
+
exclude_images: List of provider names to exclude from image generation selection.
|
|
1272
|
+
Names are case-insensitive. Optional.
|
|
1273
|
+
print_provider_info: If True, prints selected provider name and model to stdout
|
|
1274
|
+
before each request. Useful for debugging. Default is False.
|
|
1275
|
+
**kwargs: Additional keyword arguments stored for future use.
|
|
1276
|
+
|
|
1277
|
+
Examples:
|
|
1278
|
+
>>> # Minimal setup - use default providers
|
|
1279
|
+
>>> client = Client()
|
|
1280
|
+
|
|
1281
|
+
>>> # With authentication and custom settings
|
|
1282
|
+
>>> client = Client(
|
|
1283
|
+
... api_key="sk-1234567890abcdef",
|
|
1284
|
+
... proxies={"http": "http://proxy.example.com:8080"},
|
|
1285
|
+
... exclude=["UnreliableProvider"],
|
|
1286
|
+
... print_provider_info=True
|
|
1287
|
+
... )
|
|
1288
|
+
|
|
1289
|
+
>>> # With specific default providers
|
|
1290
|
+
>>> from webscout.Provider.OPENAI.groq import Groq
|
|
1291
|
+
>>> from webscout.Provider.TTI.stable import StableDiffusion
|
|
1292
|
+
>>> client = Client(
|
|
1293
|
+
... provider=Groq,
|
|
1294
|
+
... image_provider=StableDiffusion
|
|
1295
|
+
... )
|
|
1296
|
+
"""
|
|
1297
|
+
self.provider = provider
|
|
1298
|
+
self.image_provider = image_provider
|
|
1299
|
+
self.api_key = api_key
|
|
1300
|
+
self.proxies = proxies or {}
|
|
1301
|
+
self.exclude = [e.upper() if e else e for e in (exclude or [])]
|
|
1302
|
+
self.exclude_images = [e.upper() if e else e for e in (exclude_images or [])]
|
|
1303
|
+
self.print_provider_info = print_provider_info
|
|
1304
|
+
self.kwargs = kwargs
|
|
1305
|
+
|
|
1306
|
+
self._provider_cache = {}
|
|
1307
|
+
self.chat = ClientChat(self)
|
|
1308
|
+
self.images = ClientImages(self)
|
|
1309
|
+
|
|
1310
|
+
@staticmethod
|
|
1311
|
+
def get_chat_providers() -> List[str]:
|
|
1312
|
+
"""
|
|
1313
|
+
Returns a list of all available chat provider names.
|
|
1314
|
+
|
|
1315
|
+
Queries the global OPENAI_PROVIDERS registry that is populated
|
|
1316
|
+
at module load time. Names are not normalized and appear as
|
|
1317
|
+
defined in their respective classes.
|
|
1318
|
+
|
|
1319
|
+
Returns:
|
|
1320
|
+
List of provider class names available for chat completions.
|
|
1321
|
+
|
|
1322
|
+
Examples:
|
|
1323
|
+
>>> providers = Client.get_chat_providers()
|
|
1324
|
+
>>> print("GPT4Free" in providers)
|
|
1325
|
+
True
|
|
1326
|
+
>>> print(len(providers))
|
|
1327
|
+
42
|
|
1328
|
+
"""
|
|
1329
|
+
return list(OPENAI_PROVIDERS.keys())
|
|
1330
|
+
|
|
1331
|
+
@staticmethod
|
|
1332
|
+
def get_image_providers() -> List[str]:
|
|
1333
|
+
"""
|
|
1334
|
+
Returns a list of all available image provider names.
|
|
1335
|
+
|
|
1336
|
+
Queries the global TTI_PROVIDERS registry that is populated
|
|
1337
|
+
at module load time. Names are not normalized and appear as
|
|
1338
|
+
defined in their respective classes.
|
|
1339
|
+
|
|
1340
|
+
Returns:
|
|
1341
|
+
List of provider class names available for image generation.
|
|
1342
|
+
|
|
1343
|
+
Examples:
|
|
1344
|
+
>>> providers = Client.get_image_providers()
|
|
1345
|
+
>>> print("StableDiffusion" in providers)
|
|
1346
|
+
True
|
|
1347
|
+
>>> print(len(providers))
|
|
1348
|
+
8
|
|
1349
|
+
"""
|
|
1350
|
+
return list(TTI_PROVIDERS.keys())
|
|
1351
|
+
|
|
1352
|
+
@staticmethod
|
|
1353
|
+
def get_free_chat_providers() -> List[str]:
|
|
1354
|
+
"""
|
|
1355
|
+
Returns a list of chat providers that don't require authentication.
|
|
1356
|
+
|
|
1357
|
+
Filters the global OPENAI_PROVIDERS registry to include only providers
|
|
1358
|
+
where required_auth is False. These providers can be used without
|
|
1359
|
+
an API key.
|
|
1360
|
+
|
|
1361
|
+
Returns:
|
|
1362
|
+
List of free chat provider class names.
|
|
1363
|
+
|
|
1364
|
+
Examples:
|
|
1365
|
+
>>> free_providers = Client.get_free_chat_providers()
|
|
1366
|
+
>>> print("GPT4Free" in free_providers)
|
|
1367
|
+
True
|
|
1368
|
+
>>> print(len(free_providers))
|
|
1369
|
+
35
|
|
1370
|
+
"""
|
|
1371
|
+
return [name for name in OPENAI_PROVIDERS.keys() if name not in OPENAI_AUTH_REQUIRED]
|
|
1372
|
+
|
|
1373
|
+
@staticmethod
|
|
1374
|
+
def get_free_image_providers() -> List[str]:
|
|
1375
|
+
"""
|
|
1376
|
+
Returns a list of image providers that don't require authentication.
|
|
1377
|
+
|
|
1378
|
+
Filters the global TTI_PROVIDERS registry to include only providers
|
|
1379
|
+
where required_auth is False. These providers can be used without
|
|
1380
|
+
an API key.
|
|
1381
|
+
|
|
1382
|
+
Returns:
|
|
1383
|
+
List of free image provider class names.
|
|
1384
|
+
|
|
1385
|
+
Examples:
|
|
1386
|
+
>>> free_providers = Client.get_free_image_providers()
|
|
1387
|
+
>>> print("StableDiffusion" in free_providers)
|
|
1388
|
+
True
|
|
1389
|
+
>>> print(len(free_providers))
|
|
1390
|
+
6
|
|
1391
|
+
"""
|
|
1392
|
+
return [name for name in TTI_PROVIDERS.keys() if name not in TTI_AUTH_REQUIRED]
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
try:
|
|
1396
|
+
from webscout.server.server import run_api as _run_api_impl
|
|
1397
|
+
from webscout.server.server import run_api as _start_server_impl
|
|
1398
|
+
|
|
1399
|
+
def run_api(*args: Any, **kwargs: Any) -> Any:
|
|
1400
|
+
"""
|
|
1401
|
+
Runs the FastAPI OpenAI-compatible API server.
|
|
1402
|
+
|
|
1403
|
+
Delegates to webscout.server.server.run_api to start an OpenAI-compatible
|
|
1404
|
+
HTTP API server that provides chat and image endpoints. Requires the
|
|
1405
|
+
'api' optional dependencies to be installed.
|
|
1406
|
+
|
|
1407
|
+
Args:
|
|
1408
|
+
*args: Positional arguments passed to the underlying run_api implementation.
|
|
1409
|
+
**kwargs: Keyword arguments passed to the underlying run_api implementation.
|
|
1410
|
+
Common options include host, port, debug, and reload.
|
|
1411
|
+
|
|
1412
|
+
Returns:
|
|
1413
|
+
The return value from the underlying FastAPI run function.
|
|
1414
|
+
|
|
1415
|
+
Raises:
|
|
1416
|
+
ImportError: If webscout.server.server is not available.
|
|
1417
|
+
|
|
1418
|
+
Examples:
|
|
1419
|
+
>>> from webscout.client import run_api
|
|
1420
|
+
>>> run_api(host="0.0.0.0", port=8000)
|
|
1421
|
+
"""
|
|
1422
|
+
return _run_api_impl(*args, **kwargs)
|
|
1423
|
+
|
|
1424
|
+
def start_server(*args: Any, **kwargs: Any) -> Any:
|
|
1425
|
+
"""
|
|
1426
|
+
Starts the FastAPI OpenAI-compatible API server.
|
|
1427
|
+
|
|
1428
|
+
Delegates to webscout.server.server.start_server to initialize and run
|
|
1429
|
+
an OpenAI-compatible HTTP API server. This is typically the main entry
|
|
1430
|
+
point for starting the webscout server in production or development.
|
|
1431
|
+
|
|
1432
|
+
Args:
|
|
1433
|
+
*args: Positional arguments passed to the underlying start_server implementation.
|
|
1434
|
+
**kwargs: Keyword arguments passed to the underlying start_server implementation.
|
|
1435
|
+
Common options include host, port, workers, and config paths.
|
|
1436
|
+
|
|
1437
|
+
Returns:
|
|
1438
|
+
The return value from the underlying server implementation.
|
|
1439
|
+
|
|
1440
|
+
Raises:
|
|
1441
|
+
ImportError: If webscout.server.server is not available.
|
|
1442
|
+
|
|
1443
|
+
Examples:
|
|
1444
|
+
>>> from webscout.client import start_server
|
|
1445
|
+
>>> start_server()
|
|
1446
|
+
"""
|
|
1447
|
+
return _start_server_impl(*args, **kwargs)
|
|
1448
|
+
|
|
1449
|
+
except ImportError:
|
|
1450
|
+
|
|
1451
|
+
def run_api(*args: Any, **kwargs: Any) -> Any:
|
|
1452
|
+
"""
|
|
1453
|
+
Runs the FastAPI OpenAI-compatible API server.
|
|
1454
|
+
|
|
1455
|
+
Raises ImportError if the server module is not available.
|
|
1456
|
+
Install with: pip install webscout[api]
|
|
1457
|
+
|
|
1458
|
+
Raises:
|
|
1459
|
+
ImportError: Always raised; server not available in current environment.
|
|
1460
|
+
"""
|
|
1461
|
+
raise ImportError("webscout.server.server.run_api is not available.")
|
|
1462
|
+
|
|
1463
|
+
def start_server(*args: Any, **kwargs: Any) -> Any:
|
|
1464
|
+
"""
|
|
1465
|
+
Starts the FastAPI OpenAI-compatible API server.
|
|
1466
|
+
|
|
1467
|
+
Raises ImportError if the server module is not available.
|
|
1468
|
+
Install with: pip install webscout[api]
|
|
1469
|
+
|
|
1470
|
+
Raises:
|
|
1471
|
+
ImportError: Always raised; server not available in current environment.
|
|
1472
|
+
"""
|
|
1473
|
+
raise ImportError("webscout.server.server.start_server is not available.")
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
if __name__ == "__main__":
|
|
1477
|
+
client = Client(print_provider_info=True)
|
|
1478
|
+
print("Testing auto resolution...")
|
|
1479
|
+
try:
|
|
1480
|
+
response = client.chat.completions.create(
|
|
1481
|
+
model="auto", messages=[{"role": "user", "content": "Hi"}]
|
|
1482
|
+
)
|
|
1483
|
+
if not inspect.isgenerator(response):
|
|
1484
|
+
completion = cast(ChatCompletion, response)
|
|
1485
|
+
if (
|
|
1486
|
+
completion
|
|
1487
|
+
and completion.choices
|
|
1488
|
+
and completion.choices[0].message
|
|
1489
|
+
and completion.choices[0].message.content
|
|
1490
|
+
):
|
|
1491
|
+
print(f"Auto Result: {completion.choices[0].message.content[:50]}...")
|
|
1492
|
+
else:
|
|
1493
|
+
print("Auto Result: Empty response")
|
|
1494
|
+
else:
|
|
1495
|
+
print("Streaming response received")
|
|
1496
|
+
except Exception as e:
|
|
1497
|
+
print(f"Error: {e}")
|