mirror of
https://github.com/xtekky/gpt4free.git
synced 2025-12-06 02:30:41 -08:00
feat: add new providers and update models (#3075)
* feat: add agent CLI, new providers, and update models
- Add a new `agent` mode to the CLI, a feature-rich AI coding assistant with capabilities for file system operations, code execution, git integration, and interactive chat.
- Add new provider `OperaAria` with support for vision, streaming, and conversation history.
- Add new provider `Startnest` with support for `gpt-4o-mini`, vision, and streaming.
- Move providers `FreeGpt` and `Websim` to the `not_working` directory.
- Delete the `OIVSCodeSer2` provider.
- Rename the CLI `client` mode to `chat` and refactor its argument parsing.
- In `g4f/Provider/DeepInfraChat.py`:
- Add a new `api_endpoint` attribute.
- Extensively update and reorganize the `models` list and `model_aliases` dictionary with numerous new models.
- In `g4f/Provider/LambdaChat.py`:
- Change the `default_model` to `deepseek-v3-0324`.
- In `g4f/Provider/Together.py`:
- Update aliases for `llama-3.1-405b`, `deepseek-r1`, and `flux`.
- Add new models including `gemma-3-27b`, `gemma-3n-e4b`, and `qwen-3-32b`.
- In `g4f/models.py`:
- Add new model definitions for `aria`, `deepseek_v3_0324_turbo`, `deepseek_r1_0528_turbo`, and several `gemma` variants.
- Remove the `mixtral_8x22b` model definition.
- Update the `best_provider` lists for `default`, `default_vision`, `gpt_4o_mini`, `gemini-1.5-pro`, `gemini-1.5-flash`, and others to reflect provider changes.
- In `g4f/Provider/__init__.py`:
- Add `OperaAria` and `Startnest` to the list of imported providers.
- Remove `FreeGpt`, `OIVSCodeSer2`, and `Websim` from imports.
- In `requirements.txt`:
- Add `rich` as a new dependency for the agent CLI.
* feat: add gemma-3-4b alias to DeepInfraChat
In g4f/Provider/DeepInfraChat.py, add the gemma-3-4b alias to the model_aliases dictionary.
The new alias points to the google/gemma-3-4b-it model.
* feat: add OIVSCodeSer2 provider
- Create the new provider file .
- The provider supports the model and includes a custom method to generate a .
- Import and include in .
- Add to the for in .
- Add to in .
* feat: add OIVSCodeSer2 provider
- Create the new provider file .
- The provider supports the model and includes a custom method to generate a .
- Import and include in .
- Add to the for in .
- Add to in .
* refactor: Migrate from duckduckgo-search to ddgs library
* Replaced the dependency with the new library.
* In , updated imports from to and to .
* Modified the function in to use an context manager instead of a global instance.
* Updated the function to catch the new exception.
* In , updated the web search import and installation instructions to use .
* Removed unnecessary comments and simplified f-string formatting in and .
* Added and to .
* Added , , and to the extras in .
* test: Update web search tests for ddgs library and cleanup code
* In `etc/unittest/web_search.py`, updated imports from `duckduckgo_search` to `ddgs` and `DDGSError`.
* Added an import for `MissingRequirementsError` in `etc/unittest/web_search.py`.
* Modified exception handling in web search tests to catch both `DDGSError` and `MissingRequirementsError`.
* Removed temporary modification comments in `g4f/cli/agent/agent.py` and `g4f/tools/web_search.py`.
* fix: remove unstable CLI feature due to critical errors
- Remove the experimental CLI coding assistant feature due to multiple stability issues and critical errors in production environments
- Delete the entire `g4f/cli/agent/` directory and all related functionality
- Remove `rich` dependency from `requirements.txt` as it was only used by the removed feature
- Remove `rich` from the `all` extras in `setup.py`
- Revert CLI mode naming from `chat` back to `client` for consistency
- Clean up argument parsing in CLI to remove references to the removed functionality
- Remove installation instructions and imports related to the unstable feature from documentation
This removal is necessary due to:
- Unpredictable behavior causing data loss risks
- Incompatibility with certain system configurations
- Security concerns with unrestricted file system access
- Excessive resource consumption in production environments
Note: This feature may be reintroduced in the future with a more stable and secure implementation that addresses the current limitations and safety concerns.
---------
Co-authored-by: kqlio67 <kqlio67@users.noreply.github.com>
This commit is contained in:
parent
85bd2cbf28
commit
7965487830
16 changed files with 813 additions and 219 deletions
|
|
@ -4,14 +4,14 @@ import json
|
|||
import unittest
|
||||
|
||||
try:
|
||||
from duckduckgo_search import DDGS
|
||||
from duckduckgo_search.exceptions import DuckDuckGoSearchException
|
||||
from ddgs import DDGS, DDGSError
|
||||
from bs4 import BeautifulSoup
|
||||
has_requirements = True
|
||||
except ImportError:
|
||||
has_requirements = False
|
||||
|
||||
from g4f.client import AsyncClient
|
||||
from g4f.errors import MissingRequirementsError
|
||||
from .mocks import YieldProviderMock
|
||||
|
||||
DEFAULT_MESSAGES = [{'role': 'user', 'content': 'Hello'}]
|
||||
|
|
@ -45,8 +45,8 @@ class TestIterListProvider(unittest.IsolatedAsyncioTestCase):
|
|||
try:
|
||||
response = await client.chat.completions.create([{"content": "", "role": "user"}], "", tool_calls=tool_calls)
|
||||
self.assertIn("Using the provided web search results", response.choices[0].message.content)
|
||||
except DuckDuckGoSearchException as e:
|
||||
self.skipTest(f'DuckDuckGoSearchException: {e}')
|
||||
except (DDGSError, MissingRequirementsError) as e:
|
||||
self.skipTest(f'Search error: {e}')
|
||||
|
||||
async def test_search2(self):
|
||||
client = AsyncClient(provider=YieldProviderMock)
|
||||
|
|
@ -64,8 +64,8 @@ class TestIterListProvider(unittest.IsolatedAsyncioTestCase):
|
|||
try:
|
||||
response = await client.chat.completions.create([{"content": "", "role": "user"}], "", tool_calls=tool_calls)
|
||||
self.assertIn("Using the provided web search results", response.choices[0].message.content)
|
||||
except DuckDuckGoSearchException as e:
|
||||
self.skipTest(f'DuckDuckGoSearchException: {e}')
|
||||
except (DDGSError, MissingRequirementsError) as e:
|
||||
self.skipTest(f'Search error: {e}')
|
||||
|
||||
async def test_search3(self):
|
||||
client = AsyncClient(provider=YieldProviderMock)
|
||||
|
|
@ -85,5 +85,5 @@ class TestIterListProvider(unittest.IsolatedAsyncioTestCase):
|
|||
try:
|
||||
response = await client.chat.completions.create([{"content": "", "role": "user"}], "", tool_calls=tool_calls)
|
||||
self.assertIn("Using the provided web search results", response.choices[0].message.content)
|
||||
except DuckDuckGoSearchException as e:
|
||||
self.skipTest(f'DuckDuckGoSearchException: {e}')
|
||||
except (DDGSError, MissingRequirementsError) as e:
|
||||
self.skipTest(f'Search error: {e}')
|
||||
|
|
|
|||
|
|
@ -11,80 +11,134 @@ class DeepInfraChat(OpenaiTemplate):
|
|||
url = "https://deepinfra.com/chat"
|
||||
login_url = "https://deepinfra.com/dash/api_keys"
|
||||
api_base = "https://api.deepinfra.com/v1/openai"
|
||||
api_endpoint = "https://api.deepinfra.com/v1/openai/chat/completions"
|
||||
working = True
|
||||
|
||||
default_model = 'deepseek-ai/DeepSeek-V3-0324'
|
||||
default_vision_model = 'microsoft/Phi-4-multimodal-instruct'
|
||||
vision_models = [default_vision_model, 'meta-llama/Llama-3.2-90B-Vision-Instruct']
|
||||
vision_models = [
|
||||
default_vision_model,
|
||||
'meta-llama/Llama-3.2-90B-Vision-Instruct'
|
||||
]
|
||||
models = [
|
||||
'deepseek-ai/DeepSeek-R1-0528',
|
||||
'deepseek-ai/DeepSeek-Prover-V2-671B',
|
||||
'Qwen/Qwen3-235B-A22B',
|
||||
'Qwen/Qwen3-30B-A3B',
|
||||
'Qwen/Qwen3-32B',
|
||||
'Qwen/Qwen3-14B',
|
||||
'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8',
|
||||
'meta-llama/Llama-4-Scout-17B-16E-Instruct',
|
||||
'microsoft/phi-4-reasoning-plus',
|
||||
'microsoft/meta-llama/Llama-Guard-4-12B',
|
||||
'Qwen/QwQ-32B',
|
||||
# cognitivecomputations
|
||||
'cognitivecomputations/dolphin-2.6-mixtral-8x7b',
|
||||
'cognitivecomputations/dolphin-2.9.1-llama-3-70b',
|
||||
|
||||
# deepinfra
|
||||
'deepinfra/airoboros-70b',
|
||||
|
||||
# deepseek-ai
|
||||
default_model,
|
||||
'google/gemma-3-27b-it',
|
||||
'google/gemma-3-12b-it',
|
||||
'meta-llama/Meta-Llama-3.1-8B-Instruct',
|
||||
'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
||||
'deepseek-ai/DeepSeek-V3-0324-Turbo',
|
||||
|
||||
'deepseek-ai/DeepSeek-R1-0528-Turbo',
|
||||
'deepseek-ai/DeepSeek-R1-0528',
|
||||
|
||||
'deepseek-ai/DeepSeek-Prover-V2-671B',
|
||||
|
||||
'deepseek-ai/DeepSeek-V3',
|
||||
'mistralai/Mistral-Small-24B-Instruct-2501',
|
||||
|
||||
'deepseek-ai/DeepSeek-R1',
|
||||
'deepseek-ai/DeepSeek-R1-Turbo',
|
||||
'deepseek-ai/DeepSeek-R1-Distill-Llama-70B',
|
||||
'deepseek-ai/DeepSeek-R1-Distill-Qwen-32B',
|
||||
'microsoft/phi-4',
|
||||
'microsoft/WizardLM-2-8x22B',
|
||||
'Qwen/Qwen2.5-72B-Instruct',
|
||||
'Qwen/Qwen2-72B-Instruct',
|
||||
'cognitivecomputations/dolphin-2.6-mixtral-8x7b',
|
||||
'cognitivecomputations/dolphin-2.9.1-llama-3-70b',
|
||||
'deepinfra/airoboros-70b',
|
||||
|
||||
# google (gemma)
|
||||
'google/gemma-1.1-7b-it',
|
||||
'google/gemma-2-9b-it',
|
||||
'google/gemma-2-27b-it',
|
||||
'google/gemma-3-4b-it',
|
||||
'google/gemma-3-12b-it',
|
||||
'google/gemma-3-27b-it',
|
||||
|
||||
# google (codegemma)
|
||||
'google/codegemma-7b-it',
|
||||
|
||||
# lizpreciatior
|
||||
'lizpreciatior/lzlv_70b_fp16_hf',
|
||||
|
||||
# meta-llama
|
||||
'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8',
|
||||
'meta-llama/Llama-4-Scout-17B-16E-Instruct',
|
||||
'meta-llama/Meta-Llama-3.1-8B-Instruct',
|
||||
'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
||||
'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
|
||||
|
||||
# microsoft
|
||||
'microsoft/phi-4-reasoning-plus',
|
||||
'microsoft/phi-4',
|
||||
|
||||
'microsoft/WizardLM-2-8x22B',
|
||||
'microsoft/WizardLM-2-7B',
|
||||
'mistralai/Mixtral-8x22B-Instruct-v0.1',
|
||||
|
||||
# mistralai
|
||||
'mistralai/Mistral-Small-3.1-24B-Instruct-2503',
|
||||
|
||||
# Qwen
|
||||
'Qwen/Qwen3-235B-A22B',
|
||||
'Qwen/Qwen3-30B-A3B',
|
||||
'Qwen/Qwen3-32B',
|
||||
'Qwen/Qwen3-14B',
|
||||
'Qwen/QwQ-32B',
|
||||
] + vision_models
|
||||
|
||||
model_aliases = {
|
||||
"deepseek-r1-0528": "deepseek-ai/DeepSeek-R1-0528",
|
||||
"deepseek-prover-v2-671b": "deepseek-ai/DeepSeek-Prover-V2-671B",
|
||||
# cognitivecomputations
|
||||
"dolphin-2.6": "cognitivecomputations/dolphin-2.6-mixtral-8x7b",
|
||||
"dolphin-2.9": "cognitivecomputations/dolphin-2.9.1-llama-3-70b",
|
||||
|
||||
# deepinfra
|
||||
"airoboros-70b": "deepinfra/airoboros-70b",
|
||||
|
||||
# deepseek-ai
|
||||
"deepseek-prover-v2": "deepseek-ai/DeepSeek-Prover-V2-671B",
|
||||
"qwen-3-235b": "Qwen/Qwen3-235B-A22B",
|
||||
"qwen-3-30b": "Qwen/Qwen3-30B-A3B",
|
||||
"qwen-3-32b": "Qwen/Qwen3-32B",
|
||||
"qwen-3-14b": "Qwen/Qwen3-14B",
|
||||
"llama-4-maverick": "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
|
||||
"llama-4-scout": "meta-llama/Llama-4-Scout-17B-16E-Instruct",
|
||||
"phi-4-reasoning-plus": "microsoft/phi-4-reasoning-plus",
|
||||
#"": "meta-llama/Llama-Guard-4-12B",
|
||||
"qwq-32b": "Qwen/QwQ-32B",
|
||||
"deepseek-prover-v2-671b": "deepseek-ai/DeepSeek-Prover-V2-671B",
|
||||
"deepseek-r1": ["deepseek-ai/DeepSeek-R1", "deepseek-ai/DeepSeek-R1-0528"],
|
||||
"deepseek-r1-0528": "deepseek-ai/DeepSeek-R1-0528",
|
||||
"deepseek-r1-0528-turbo": "deepseek-ai/DeepSeek-R1-0528-Turbo",
|
||||
"deepseek-r1-distill-llama-70b": "deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
|
||||
"deepseek-r1-distill-qwen-32b": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
|
||||
"deepseek-r1-turbo": "deepseek-ai/DeepSeek-R1-Turbo",
|
||||
"deepseek-v3": ["deepseek-ai/DeepSeek-V3", "deepseek-ai/DeepSeek-V3-0324"],
|
||||
"deepseek-v3-0324": "deepseek-ai/DeepSeek-V3-0324",
|
||||
"gemma-3-27b": "google/gemma-3-27b-it",
|
||||
"deepseek-v3-0324-turbo": "deepseek-ai/DeepSeek-V3-0324-Turbo",
|
||||
|
||||
# google
|
||||
"codegemma-7b": "google/codegemma-7b-it",
|
||||
"gemma-1.1-7b": "google/gemma-1.1-7b-it",
|
||||
"gemma-2-27b": "google/gemma-2-27b-it",
|
||||
"gemma-2-9b": "google/gemma-2-9b-it",
|
||||
"gemma-3-4b": "google/gemma-3-4b-it",
|
||||
"gemma-3-12b": "google/gemma-3-12b-it",
|
||||
"phi-4-multimodal": "microsoft/Phi-4-multimodal-instruct",
|
||||
"gemma-3-27b": "google/gemma-3-27b-it",
|
||||
|
||||
# lizpreciatior
|
||||
"lzlv-70b": "lizpreciatior/lzlv_70b_fp16_hf",
|
||||
|
||||
# meta-llama
|
||||
"llama-3.1-8b": "meta-llama/Meta-Llama-3.1-8B-Instruct",
|
||||
"llama-3.2-90b": "meta-llama/Llama-3.2-90B-Vision-Instruct",
|
||||
"llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct",
|
||||
"mistral-small-24b": "mistralai/Mistral-Small-24B-Instruct-2501",
|
||||
"deepseek-r1-turbo": "deepseek-ai/DeepSeek-R1-Turbo",
|
||||
"deepseek-r1": ["deepseek-ai/DeepSeek-R1", "deepseek-ai/DeepSeek-R1-0528"],
|
||||
"deepseek-r1-distill-llama-70b": "deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
|
||||
"deepseek-r1-distill-qwen-32b": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
|
||||
"llama-4-maverick": "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
|
||||
"llama-4-scout": "meta-llama/Llama-4-Scout-17B-16E-Instruct",
|
||||
|
||||
# microsoft
|
||||
"phi-4": "microsoft/phi-4",
|
||||
"wizardlm-2-8x22b": "microsoft/WizardLM-2-8x22B",
|
||||
"qwen-2-72b": "Qwen/Qwen2-72B-Instruct",
|
||||
"dolphin-2.6": "cognitivecomputations/dolphin-2.6-mixtral-8x7b",
|
||||
"dolphin-2.9": "cognitivecomputations/dolphin-2.9.1-llama-3-70b",
|
||||
"airoboros-70b": "deepinfra/airoboros-70b",
|
||||
"lzlv-70b": "lizpreciatior/lzlv_70b_fp16_hf",
|
||||
"phi-4-multimodal": default_vision_model,
|
||||
"phi-4-reasoning-plus": "microsoft/phi-4-reasoning-plus",
|
||||
"wizardlm-2-7b": "microsoft/WizardLM-2-7B",
|
||||
"mixtral-8x22b": "mistralai/Mixtral-8x22B-Instruct-v0.1"
|
||||
"wizardlm-2-8x22b": "microsoft/WizardLM-2-8x22B",
|
||||
|
||||
# mistralai
|
||||
"mistral-small-3.1-24b": "mistralai/Mistral-Small-3.1-24B-Instruct-2503",
|
||||
|
||||
# Qwen
|
||||
"qwen-3-14b": "Qwen/Qwen3-14B",
|
||||
"qwen-3-30b": "Qwen/Qwen3-30B-A3B",
|
||||
"qwen-3-32b": "Qwen/Qwen3-32B",
|
||||
"qwen-3-235b": "Qwen/Qwen3-235B-A22B",
|
||||
"qwq-32b": "Qwen/QwQ-32B",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class LambdaChat(AsyncGeneratorProvider, ProviderModelMixin):
|
|||
|
||||
working = True
|
||||
|
||||
default_model = "deepseek-r1"
|
||||
default_model = "deepseek-v3-0324"
|
||||
models = [
|
||||
"deepseek-llama3.3-70b",
|
||||
"deepseek-r1",
|
||||
|
|
@ -35,6 +35,7 @@ class LambdaChat(AsyncGeneratorProvider, ProviderModelMixin):
|
|||
"llama3.3-70b-instruct-fp8",
|
||||
"qwen25-coder-32b-instruct",
|
||||
"deepseek-v3",
|
||||
default_model,
|
||||
"llama-4-maverick-17b-128e-instruct-fp8",
|
||||
"llama-4-scout-17b-16e-instruct",
|
||||
"llama3.3-70b-instruct-fp8",
|
||||
|
|
|
|||
330
g4f/Provider/OperaAria.py
Executable file
330
g4f/Provider/OperaAria.py
Executable file
|
|
@ -0,0 +1,330 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import re
|
||||
import os
|
||||
import base64
|
||||
import asyncio
|
||||
from aiohttp import ClientSession, FormData
|
||||
|
||||
from ..typing import AsyncResult, Messages, MediaListType
|
||||
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
|
||||
from .helper import format_prompt
|
||||
from ..providers.response import JsonConversation, FinishReason, ImageResponse
|
||||
from ..image import to_data_uri, is_data_an_media
|
||||
from ..tools.media import merge_media
|
||||
|
||||
|
||||
class Conversation(JsonConversation):
|
||||
"""Manages all session-specific state for Opera Aria."""
|
||||
access_token: str = None
|
||||
refresh_token: str = None
|
||||
encryption_key: str = None
|
||||
expires_at: float = 0
|
||||
conversation_id: str = None
|
||||
is_first_request: bool = True
|
||||
|
||||
def __init__(self, refresh_token: str = None):
|
||||
"""Initializes a new session, generating a unique encryption key."""
|
||||
self.refresh_token = refresh_token
|
||||
self.encryption_key = self._generate_encryption_key()
|
||||
self.is_first_request = True
|
||||
|
||||
def is_token_expired(self) -> bool:
|
||||
"""Check if the current token has expired"""
|
||||
return time.time() >= self.expires_at
|
||||
|
||||
def update_token(self, access_token: str, expires_in: int):
|
||||
"""Update the access token and expiration time"""
|
||||
self.access_token = access_token
|
||||
self.expires_at = time.time() + expires_in - 60
|
||||
|
||||
@staticmethod
|
||||
def _generate_encryption_key() -> str:
|
||||
"""Generates a 32-byte, Base64-encoded key for the session."""
|
||||
random_bytes = os.urandom(32)
|
||||
return base64.b64encode(random_bytes).decode('utf-8')
|
||||
|
||||
@staticmethod
|
||||
def generate_conversation_id() -> str:
|
||||
"""Generate conversation ID in Opera Aria format"""
|
||||
parts = [
|
||||
''.join(random.choices('0123456789abcdef', k=8)),
|
||||
''.join(random.choices('0123456789abcdef', k=4)),
|
||||
'11f0',
|
||||
''.join(random.choices('0123456789abcdef', k=4)),
|
||||
''.join(random.choices('0123456789abcdef', k=12))
|
||||
]
|
||||
return '-'.join(parts)
|
||||
|
||||
|
||||
class OperaAria(AsyncGeneratorProvider, ProviderModelMixin):
|
||||
label = "Opera Aria"
|
||||
url = "https://play.google.com/store/apps/details?id=com.opera.browser"
|
||||
api_endpoint = "https://composer.opera-api.com/api/v1/a-chat"
|
||||
token_endpoint = "https://oauth2.opera-api.com/oauth2/v1/token/"
|
||||
signup_endpoint = "https://auth.opera.com/account/v2/external/anonymous/signup"
|
||||
upload_endpoint = "https://composer.opera-api.com/api/v1/images/upload"
|
||||
check_status_endpoint = "https://composer.opera-api.com/api/v1/images/check-status/"
|
||||
|
||||
working = True
|
||||
needs_auth = False
|
||||
supports_stream = True
|
||||
supports_system_message = True
|
||||
supports_message_history = True
|
||||
|
||||
default_model = 'aria'
|
||||
default_image_model = 'aria'
|
||||
image_models = ['aria']
|
||||
default_vision_model = 'aria'
|
||||
vision_models = ['aria']
|
||||
models = ['aria']
|
||||
|
||||
@classmethod
|
||||
async def _generate_refresh_token(cls, session: ClientSession) -> str:
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36 OPR/89.0.0.0",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
data = {
|
||||
"client_id": "ofa-client",
|
||||
"client_secret": "N9OscfA3KxlJASuIe29PGZ5RpWaMTBoy",
|
||||
"grant_type": "client_credentials",
|
||||
"scope": "anonymous_account"
|
||||
}
|
||||
async with session.post(cls.token_endpoint, headers=headers, data=data) as response:
|
||||
response.raise_for_status()
|
||||
anonymous_token_data = await response.json()
|
||||
anonymous_access_token = anonymous_token_data["access_token"]
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla 5.0 (Linux; Android 14) com.opera.browser OPR/89.5.4705.84314",
|
||||
"Authorization": f"Bearer {anonymous_access_token}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
}
|
||||
data = {"client_id": "ofa", "service": "aria"}
|
||||
async with session.post(cls.signup_endpoint, headers=headers, json=data) as response:
|
||||
response.raise_for_status()
|
||||
signup_data = await response.json()
|
||||
auth_token = signup_data["token"]
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36 OPR/89.0.0.0",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
data = {
|
||||
"auth_token": auth_token,
|
||||
"client_id": "ofa",
|
||||
"device_name": "GPT4FREE",
|
||||
"grant_type": "auth_token",
|
||||
"scope": "ALL"
|
||||
}
|
||||
async with session.post(cls.token_endpoint, headers=headers, data=data) as response:
|
||||
response.raise_for_status()
|
||||
final_token_data = await response.json()
|
||||
return final_token_data["refresh_token"]
|
||||
|
||||
@classmethod
|
||||
def get_model(cls, model: str) -> str:
|
||||
return cls.model_aliases.get(model, cls.default_model)
|
||||
|
||||
@classmethod
|
||||
async def get_access_token(cls, session: ClientSession, conversation: Conversation) -> str:
|
||||
if not conversation.refresh_token:
|
||||
conversation.refresh_token = await cls._generate_refresh_token(session)
|
||||
|
||||
if conversation.access_token and not conversation.is_token_expired():
|
||||
return conversation.access_token
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36 OPR/89.0.0.0"
|
||||
}
|
||||
data = {
|
||||
"client_id": "ofa",
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": conversation.refresh_token,
|
||||
"scope": "shodan:aria user:read"
|
||||
}
|
||||
async with session.post(cls.token_endpoint, headers=headers, data=data) as response:
|
||||
response.raise_for_status()
|
||||
result = await response.json()
|
||||
conversation.update_token(
|
||||
access_token=result["access_token"],
|
||||
expires_in=result.get("expires_in", 3600)
|
||||
)
|
||||
return result["access_token"]
|
||||
|
||||
@classmethod
|
||||
async def check_upload_status(cls, session: ClientSession, access_token: str, image_id: str, max_attempts: int = 30):
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36 OPR/89.0.0.0",
|
||||
}
|
||||
url = f"{cls.check_status_endpoint}{image_id}"
|
||||
for _ in range(max_attempts):
|
||||
async with session.get(url, headers=headers) as response:
|
||||
response.raise_for_status()
|
||||
result = await response.json()
|
||||
if result.get("status") == "ok":
|
||||
return
|
||||
if result.get("status") == "failed":
|
||||
raise Exception(f"Image upload failed for {image_id}")
|
||||
await asyncio.sleep(0.5)
|
||||
raise Exception(f"Timeout waiting for image upload status for {image_id}")
|
||||
|
||||
@classmethod
|
||||
async def upload_media(cls, session: ClientSession, access_token: str, media_data: bytes, filename: str) -> str:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Origin": "opera-aria://ui",
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36 OPR/89.0.0.0",
|
||||
}
|
||||
form_data = FormData()
|
||||
if not filename:
|
||||
filename = str(int(time.time() * 1000))
|
||||
content_type = is_data_an_media(media_data, filename) or "application/octet-stream"
|
||||
form_data.add_field('image_file', media_data, filename=filename, content_type=content_type)
|
||||
|
||||
async with session.post(cls.upload_endpoint, headers=headers, data=form_data) as response:
|
||||
response.raise_for_status()
|
||||
result = await response.json()
|
||||
image_id = result.get("image_id")
|
||||
if not image_id:
|
||||
raise Exception("No image_id returned from upload")
|
||||
await cls.check_upload_status(session, access_token, image_id)
|
||||
return image_id
|
||||
|
||||
@classmethod
|
||||
def extract_image_urls(cls, text: str) -> list[str]:
|
||||
pattern = r'!\[\]\((https?://[^\)]+)\)'
|
||||
urls = re.findall(pattern, text)
|
||||
return [url.replace(r'\/', '/') for url in urls]
|
||||
|
||||
@classmethod
|
||||
async def create_async_generator(
|
||||
cls,
|
||||
model: str,
|
||||
messages: Messages,
|
||||
proxy: str = None,
|
||||
refresh_token: str = None,
|
||||
conversation: Conversation = None,
|
||||
return_conversation: bool = False,
|
||||
stream: bool = True,
|
||||
media: MediaListType = None,
|
||||
**kwargs
|
||||
) -> AsyncResult:
|
||||
model = cls.get_model(model)
|
||||
|
||||
if conversation is None:
|
||||
conversation = Conversation(refresh_token)
|
||||
elif refresh_token and not conversation.refresh_token:
|
||||
conversation.refresh_token = refresh_token
|
||||
|
||||
async with ClientSession() as session:
|
||||
access_token = await cls.get_access_token(session, conversation)
|
||||
|
||||
media_attachments = []
|
||||
merged_media = list(merge_media(media, messages))
|
||||
if merged_media:
|
||||
for media_data, media_name in merged_media:
|
||||
try:
|
||||
if isinstance(media_data, str) and media_data.startswith("data:"):
|
||||
data_part = media_data.split(",", 1)[1]
|
||||
media_bytes = base64.b64decode(data_part)
|
||||
elif hasattr(media_data, 'read'):
|
||||
media_bytes = media_data.read()
|
||||
elif isinstance(media_data, (str, os.PathLike)):
|
||||
with open(media_data, 'rb') as f:
|
||||
media_bytes = f.read()
|
||||
else:
|
||||
media_bytes = media_data
|
||||
|
||||
image_id = await cls.upload_media(session, access_token, media_bytes, media_name)
|
||||
media_attachments.append(image_id)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
headers = {
|
||||
"Accept": "text/event-stream" if stream else "application/json",
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "opera-aria://ui",
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36 OPR/89.0.0.0",
|
||||
"X-Opera-Timezone": "+03:00",
|
||||
"X-Opera-UI-Language": "en"
|
||||
}
|
||||
|
||||
data = {
|
||||
"query": format_prompt(messages), "stream": stream, "linkify": True,
|
||||
"linkify_version": 3, "sia": True, "media_attachments": media_attachments,
|
||||
"encryption": {"key": conversation.encryption_key}
|
||||
}
|
||||
|
||||
if not conversation.is_first_request and conversation.conversation_id:
|
||||
data["conversation_id"] = conversation.conversation_id
|
||||
|
||||
async with session.post(cls.api_endpoint, headers=headers, json=data, proxy=proxy) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
if stream:
|
||||
text_buffer, image_urls, finish_reason = [], [], None
|
||||
|
||||
async for line in response.content:
|
||||
if not line: continue
|
||||
decoded = line.decode('utf-8').strip()
|
||||
if not decoded.startswith('data: '): continue
|
||||
|
||||
content = decoded[6:]
|
||||
if content == '[DONE]': break
|
||||
|
||||
try:
|
||||
json_data = json.loads(content)
|
||||
if 'message' in json_data:
|
||||
message_chunk = json_data['message']
|
||||
found_urls = cls.extract_image_urls(message_chunk)
|
||||
if found_urls:
|
||||
image_urls.extend(found_urls)
|
||||
else:
|
||||
text_buffer.append(message_chunk)
|
||||
|
||||
if 'conversation_id' in json_data and json_data['conversation_id']:
|
||||
conversation.conversation_id = json_data['conversation_id']
|
||||
|
||||
if 'finish_reason' in json_data and json_data.get('finish_reason'):
|
||||
finish_reason = json_data['finish_reason']
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
if image_urls:
|
||||
yield ImageResponse(image_urls, format_prompt(messages))
|
||||
elif text_buffer:
|
||||
yield "".join(text_buffer)
|
||||
|
||||
if finish_reason:
|
||||
yield FinishReason(finish_reason)
|
||||
|
||||
else: # Non-streaming
|
||||
json_data = await response.json()
|
||||
if 'message' in json_data:
|
||||
message = json_data['message']
|
||||
image_urls = cls.extract_image_urls(message)
|
||||
if image_urls:
|
||||
yield ImageResponse(image_urls, format_prompt(messages))
|
||||
else:
|
||||
yield message
|
||||
|
||||
if 'conversation_id' in json_data and json_data['conversation_id']:
|
||||
conversation.conversation_id = json_data['conversation_id']
|
||||
|
||||
if 'finish_reason' in json_data and json_data['finish_reason']:
|
||||
yield FinishReason(json_data['finish_reason'])
|
||||
|
||||
conversation.is_first_request = False
|
||||
|
||||
if return_conversation:
|
||||
yield conversation
|
||||
216
g4f/Provider/Startnest.py
Executable file
216
g4f/Provider/Startnest.py
Executable file
|
|
@ -0,0 +1,216 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from aiohttp import ClientSession
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
|
||||
from ..typing import AsyncResult, Messages, MediaListType
|
||||
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
|
||||
from .helper import format_prompt
|
||||
from ..tools.media import merge_media
|
||||
from ..image import to_data_uri
|
||||
from ..providers.response import FinishReason
|
||||
|
||||
|
||||
class Startnest(AsyncGeneratorProvider, ProviderModelMixin):
|
||||
label = "Startnest"
|
||||
url = "https://play.google.com/store/apps/details?id=starnest.aitype.aikeyboard.chatbot.chatgpt"
|
||||
api_endpoint = "https://api.startnest.uk/api/completions/stream"
|
||||
|
||||
working = True
|
||||
needs_auth = False
|
||||
supports_stream = True
|
||||
supports_system_message = True
|
||||
supports_message_history = True
|
||||
|
||||
default_model = 'gpt-4o-mini'
|
||||
default_vision_model = default_model
|
||||
vision_models = [default_model, "gpt-4o-mini"]
|
||||
models = vision_models
|
||||
|
||||
@classmethod
|
||||
def generate_signature(cls, timestamp: int) -> str:
|
||||
"""
|
||||
Generate signature for authorization header
|
||||
You may need to adjust this based on the actual signature algorithm
|
||||
"""
|
||||
# This is a placeholder - the actual signature generation might involve:
|
||||
# - A secret key
|
||||
# - Specific string formatting
|
||||
# - Different hash input
|
||||
|
||||
# Example implementation (adjust as needed):
|
||||
kid = "36ccfe00-78fc-4cab-9c5b-5460b0c78513"
|
||||
algorithm = "sha256"
|
||||
validity = 90
|
||||
user_id = ""
|
||||
|
||||
# The actual signature generation logic needs to be determined
|
||||
# This is just a placeholder that creates a hash from timestamp
|
||||
signature_input = f"{kid}{timestamp}{validity}".encode()
|
||||
signature_value = hashlib.sha256(signature_input).hexdigest()
|
||||
|
||||
return f"Signature kid={kid}&algorithm={algorithm}×tamp={timestamp}&validity={validity}&userId={user_id}&value={signature_value}"
|
||||
|
||||
@classmethod
|
||||
async def create_async_generator(
|
||||
cls,
|
||||
model: str,
|
||||
messages: Messages,
|
||||
proxy: str = None,
|
||||
media: MediaListType = None,
|
||||
stream: bool = True,
|
||||
max_tokens: int = None,
|
||||
**kwargs
|
||||
) -> AsyncResult:
|
||||
model = cls.get_model(model)
|
||||
|
||||
# Generate current timestamp
|
||||
timestamp = int(time.time())
|
||||
|
||||
headers = {
|
||||
"Accept-Encoding": "gzip",
|
||||
"app_name": "AIKEYBOARD",
|
||||
"Authorization": cls.generate_signature(timestamp),
|
||||
"Connection": "Keep-Alive",
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
"Host": "api.startnest.uk",
|
||||
"User-Agent": "okhttp/4.9.0",
|
||||
}
|
||||
|
||||
async with ClientSession() as session:
|
||||
# Merge media with messages
|
||||
media = list(merge_media(media, messages))
|
||||
|
||||
# Convert messages to the required format
|
||||
formatted_messages = []
|
||||
for i, msg in enumerate(messages):
|
||||
if isinstance(msg, dict):
|
||||
role = msg.get("role", "user")
|
||||
content = msg.get("content", "")
|
||||
|
||||
# Create content array
|
||||
content_array = []
|
||||
|
||||
# Add images if this is the last user message and media exists
|
||||
if media and role == "user" and i == len(messages) - 1:
|
||||
for image, image_name in media:
|
||||
image_data_uri = to_data_uri(image)
|
||||
content_array.append({
|
||||
"image_url": {
|
||||
"url": image_data_uri
|
||||
},
|
||||
"type": "image_url"
|
||||
})
|
||||
|
||||
# Add text content
|
||||
if content:
|
||||
content_array.append({
|
||||
"text": content,
|
||||
"type": "text"
|
||||
})
|
||||
|
||||
formatted_messages.append({
|
||||
"role": role,
|
||||
"content": content_array
|
||||
})
|
||||
|
||||
# If only one message and no media, use format_prompt as requested
|
||||
if len(messages) == 1 and not media:
|
||||
prompt_text = format_prompt(messages)
|
||||
formatted_messages = [{
|
||||
"role": "user",
|
||||
"content": [{"text": prompt_text, "type": "text"}]
|
||||
}]
|
||||
|
||||
data = {
|
||||
"isVip": True,
|
||||
"max_tokens": max_tokens,
|
||||
"messages": formatted_messages,
|
||||
"stream": stream
|
||||
}
|
||||
|
||||
# Add advanceToolType if media is present
|
||||
if media:
|
||||
data["advanceToolType"] = "upload_and_ask"
|
||||
|
||||
async with session.post(cls.api_endpoint, json=data, headers=headers, proxy=proxy) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
if stream:
|
||||
# Handle streaming response (SSE format)
|
||||
async for line in response.content:
|
||||
if line:
|
||||
line = line.decode('utf-8').strip()
|
||||
if line.startswith("data: "):
|
||||
data_str = line[6:]
|
||||
if data_str == "[DONE]":
|
||||
break
|
||||
try:
|
||||
json_data = json.loads(data_str)
|
||||
if "choices" in json_data and len(json_data["choices"]) > 0:
|
||||
choice = json_data["choices"][0]
|
||||
|
||||
# Handle content
|
||||
delta = choice.get("delta", {})
|
||||
content = delta.get("content", "")
|
||||
if content:
|
||||
yield content
|
||||
|
||||
# Handle finish_reason
|
||||
if "finish_reason" in choice and choice["finish_reason"] is not None:
|
||||
yield FinishReason(choice["finish_reason"])
|
||||
break
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
else:
|
||||
# Handle non-streaming response (regular JSON)
|
||||
response_text = await response.text()
|
||||
try:
|
||||
json_data = json.loads(response_text)
|
||||
if "choices" in json_data and len(json_data["choices"]) > 0:
|
||||
choice = json_data["choices"][0]
|
||||
if "message" in choice and "content" in choice["message"]:
|
||||
content = choice["message"]["content"]
|
||||
if content:
|
||||
yield content.strip()
|
||||
|
||||
# Handle finish_reason for non-streaming
|
||||
if "finish_reason" in choice and choice["finish_reason"] is not None:
|
||||
yield FinishReason(choice["finish_reason"])
|
||||
return
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# If it's still SSE format even when stream=False, handle it
|
||||
lines = response_text.strip().split('\n')
|
||||
full_content = []
|
||||
finish_reason_value = None
|
||||
|
||||
for line in lines:
|
||||
if line.startswith("data: "):
|
||||
data_str = line[6:]
|
||||
if data_str == "[DONE]":
|
||||
break
|
||||
try:
|
||||
json_data = json.loads(data_str)
|
||||
if "choices" in json_data and len(json_data["choices"]) > 0:
|
||||
choice = json_data["choices"][0]
|
||||
delta = choice.get("delta", {})
|
||||
content = delta.get("content", "")
|
||||
if content:
|
||||
full_content.append(content)
|
||||
|
||||
# Store finish_reason
|
||||
if "finish_reason" in choice and choice["finish_reason"] is not None:
|
||||
finish_reason_value = choice["finish_reason"]
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
if full_content:
|
||||
yield ''.join(full_content)
|
||||
|
||||
if finish_reason_value:
|
||||
yield FinishReason(finish_reason_value)
|
||||
|
|
@ -58,20 +58,21 @@ class Together(OpenaiTemplate):
|
|||
#"llama-vision": "meta-llama/Llama-Vision-Free",
|
||||
"llama-3-8b": ["meta-llama/Llama-3-8b-chat-hf", "meta-llama/Meta-Llama-3-8B-Instruct-Lite", "roberizk@gmail.com/meta-llama/Meta-Llama-3-8B-Instruct-8ced8839",],
|
||||
"llama-3.1-70b": ["meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", "Rrrr/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo-03dc18e1", "Rrrr/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo-6c92f39d"],
|
||||
"llama-3.1-405b": "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo",
|
||||
"llama-3.1-405b": ["meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", "eddiehou/meta-llama/Llama-3.1-405B"],
|
||||
"llama-4-maverick": "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
|
||||
|
||||
# arcee-ai
|
||||
#"arcee-spotlight": "arcee-ai/arcee-spotlight",
|
||||
#"arcee-spotlight": "arcee_ai/arcee-spotlight",
|
||||
#"arcee-blitz": "arcee-ai/arcee-blitz",
|
||||
#"arcee-caller": "arcee-ai/caller",
|
||||
#"arcee-virtuoso-large": "arcee-ai/virtuoso-large",
|
||||
#"arcee-virtuoso-medium": "arcee-ai/virtuoso-medium-v2",
|
||||
#"arcee-coder-large": "arcee-ai/coder-large",
|
||||
#"arcee-maestro": "arcee-ai/maestro-reasoning",
|
||||
#"afm-4.5b": "arcee-ai/AFM-4.5B-Preview",
|
||||
|
||||
# deepseek-ai
|
||||
"deepseek-r1": "deepseek-ai/DeepSeek-R1",
|
||||
"deepseek-r1": ["deepseek-ai/DeepSeek-R1", "deepseek-ai/DeepSeek-R1-0528-tput"],
|
||||
"deepseek-v3": ["deepseek-ai/DeepSeek-V3", "deepseek-ai/DeepSeek-V3-p-dp"],
|
||||
"deepseek-r1-distill-llama-70b": ["deepseek-ai/DeepSeek-R1-Distill-Llama-70B", "deepseek-ai/DeepSeek-R1-Distill-Llama-70B-free"],
|
||||
"deepseek-r1-distill-qwen-1.5b": "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
|
||||
|
|
@ -86,6 +87,7 @@ class Together(OpenaiTemplate):
|
|||
"qwen-2.5-72b": "Qwen/Qwen2.5-72B-Instruct-Turbo",
|
||||
"qwen-3-235b": ["Qwen/Qwen3-235B-A22B-fp8", "Qwen/Qwen3-235B-A22B-fp8-tput"],
|
||||
"qwen-2-72b": "Qwen/Qwen2-72B-Instruct",
|
||||
"qwen-3-32b": "Qwen/Qwen3-32B-FP8",
|
||||
|
||||
# mistralai
|
||||
"mixtral-8x7b": "mistralai/Mixtral-8x7B-Instruct-v0.1",
|
||||
|
|
@ -93,8 +95,10 @@ class Together(OpenaiTemplate):
|
|||
"mistral-7b": ["mistralai/Mistral-7B-Instruct-v0.1", "mistralai/Mistral-7B-Instruct-v0.2", "mistralai/Mistral-7B-Instruct-v0.3"],
|
||||
|
||||
# google
|
||||
#"gemma-2b": "google/gemma-2b-it",
|
||||
"gemma-2b": "google/gemma-2b-it",
|
||||
"gemma-2-27b": "google/gemma-2-27b-it",
|
||||
"gemma-3-27b": "google/gemma-3-27b-it",
|
||||
"gemma-3n-e4b": "google/gemma-3n-E4B-it",
|
||||
|
||||
# nvidia
|
||||
"nemotron-70b": "nvidia/Llama-3.1-Nemotron-70B-Instruct-HF",
|
||||
|
|
@ -120,15 +124,16 @@ class Together(OpenaiTemplate):
|
|||
#"marin-8b": "marin-community/marin-8b-instruct",
|
||||
|
||||
# scb10x
|
||||
#"typhoon-2-8b": "scb10x/scb10x-llama3-1-typhoon2-8b-instruct",
|
||||
#"typhoon-2-70b": "scb10x/scb10x-llama3-1-typhoon2-70b-instruct",
|
||||
#"typhoon-2.1": "scb10x/scb10x-typhoon-2-1-gemma3-12b",
|
||||
|
||||
# perplexity-ai
|
||||
"r1-1776": "perplexity-ai/r1-1776",
|
||||
|
||||
### Models Image ###
|
||||
# black-forest-labs
|
||||
"flux": ["black-forest-labs/FLUX.1-schnell-Free", "black-forest-labs/FLUX.1-schnell", "black-forest-labs/FLUX.1.1-pro", "black-forest-labs/FLUX.1-pro", "black-forest-labs/FLUX.1-dev"],
|
||||
"flux": ["black-forest-labs/FLUX.1-schnell-Free", "black-forest-labs/FLUX.1-schnell", "black-forest-labs/FLUX.1.1-pro", "black-forest-labs/FLUX.1-pro", "black-forest-labs/FLUX.1.1-pro", "black-forest-labs/FLUX.1-pro", "black-forest-labs/FLUX.1-redux", "black-forest-labs/FLUX.1-depth", "black-forest-labs/FLUX.1-canny", "black-forest-labs/FLUX.1-kontext-max", "black-forest-labs/FLUX.1-dev-lora", "black-forest-labs/FLUX.1-dev", "black-forest-labs/FLUX.1-dev-lora", "black-forest-labs/FLUX.1-kontext-pro", "black-forest-labs/FLUX.1-kontext-dev"],
|
||||
|
||||
"flux-schnell": ["black-forest-labs/FLUX.1-schnell-Free", "black-forest-labs/FLUX.1-schnell"],
|
||||
"flux-pro": ["black-forest-labs/FLUX.1.1-pro", "black-forest-labs/FLUX.1-pro"],
|
||||
"flux-redux": "black-forest-labs/FLUX.1-redux",
|
||||
|
|
@ -137,7 +142,8 @@ class Together(OpenaiTemplate):
|
|||
"flux-kontext-max": "black-forest-labs/FLUX.1-kontext-max",
|
||||
"flux-dev-lora": "black-forest-labs/FLUX.1-dev-lora",
|
||||
"flux-dev": ["black-forest-labs/FLUX.1-dev", "black-forest-labs/FLUX.1-dev-lora"],
|
||||
"flux-kontext-pro": "black-forest-labs/FLUX.1-kontext-pro"
|
||||
"flux-kontext-pro": "black-forest-labs/FLUX.1-kontext-pro",
|
||||
"flux-kontext-dev": "black-forest-labs/FLUX.1-kontext-dev",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -40,18 +40,18 @@ from .Copilot import Copilot
|
|||
from .DeepInfraChat import DeepInfraChat
|
||||
from .DuckDuckGo import DuckDuckGo
|
||||
from .Free2GPT import Free2GPT
|
||||
from .FreeGpt import FreeGpt
|
||||
from .ImageLabs import ImageLabs
|
||||
from .LambdaChat import LambdaChat
|
||||
from .LegacyLMArena import LegacyLMArena
|
||||
from .OIVSCodeSer2 import OIVSCodeSer2
|
||||
from .OIVSCodeSer0501 import OIVSCodeSer0501
|
||||
from .OperaAria import OperaAria
|
||||
from .PerplexityLabs import PerplexityLabs
|
||||
from .PollinationsAI import PollinationsAI
|
||||
from .PollinationsImage import PollinationsImage
|
||||
from .Startnest import Startnest
|
||||
from .TeachAnything import TeachAnything
|
||||
from .Together import Together
|
||||
from .Websim import Websim
|
||||
from .WeWordle import WeWordle
|
||||
from .Yqcloud import Yqcloud
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import time
|
|||
import hashlib
|
||||
import random
|
||||
from typing import AsyncGenerator, Optional, Dict, Any
|
||||
from ..typing import Messages
|
||||
from ..requests import StreamSession, raise_for_status
|
||||
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
|
||||
from ..errors import RateLimitError
|
||||
from ...typing import Messages
|
||||
from ...requests import StreamSession, raise_for_status
|
||||
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
|
||||
from ...errors import RateLimitError
|
||||
|
||||
# Constants
|
||||
DOMAINS = [
|
||||
|
|
@ -22,7 +22,7 @@ RATE_LIMIT_ERROR_MESSAGE = "当前地区当日额度已消耗完"
|
|||
class FreeGpt(AsyncGeneratorProvider, ProviderModelMixin):
|
||||
url = "https://freegptsnav.aifree.site"
|
||||
|
||||
working = True
|
||||
working = False
|
||||
supports_message_history = True
|
||||
supports_system_message = True
|
||||
|
||||
|
|
@ -6,12 +6,12 @@ import string
|
|||
import asyncio
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from ..typing import AsyncResult, Messages
|
||||
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
|
||||
from ..requests.raise_for_status import raise_for_status
|
||||
from ..errors import ResponseStatusError
|
||||
from ..providers.response import ImageResponse
|
||||
from .helper import format_prompt, format_media_prompt
|
||||
from ...typing import AsyncResult, Messages
|
||||
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
|
||||
from ...requests.raise_for_status import raise_for_status
|
||||
from ...errors import ResponseStatusError
|
||||
from ...providers.response import ImageResponse
|
||||
from ..helper import format_prompt, format_media_prompt
|
||||
|
||||
|
||||
class Websim(AsyncGeneratorProvider, ProviderModelMixin):
|
||||
|
|
@ -20,17 +20,17 @@ class Websim(AsyncGeneratorProvider, ProviderModelMixin):
|
|||
chat_api_endpoint = "https://websim.ai/api/v1/inference/run_chat_completion"
|
||||
image_api_endpoint = "https://websim.ai/api/v1/inference/run_image_generation"
|
||||
|
||||
working = True
|
||||
working = False
|
||||
needs_auth = False
|
||||
use_nodriver = False
|
||||
supports_stream = False
|
||||
supports_system_message = True
|
||||
supports_message_history = True
|
||||
|
||||
default_model = 'gemini-1.5-pro'
|
||||
default_model = 'gemini-2.5-pro'
|
||||
default_image_model = 'flux'
|
||||
image_models = [default_image_model]
|
||||
models = [default_model, 'gemini-1.5-flash'] + image_models
|
||||
models = [default_model, 'gemini-2.5-flash'] + image_models
|
||||
|
||||
@staticmethod
|
||||
def generate_project_id(for_image=False):
|
||||
|
|
@ -15,6 +15,7 @@ from .ChatGptt import ChatGptt
|
|||
from .DDG import DDG
|
||||
from .Equing import Equing
|
||||
from .FlowGpt import FlowGpt
|
||||
from .FreeGpt import FreeGpt
|
||||
from .FreeNetfly import FreeNetfly
|
||||
from .FreeRouter import FreeRouter
|
||||
from .Glider import Glider
|
||||
|
|
@ -33,3 +34,5 @@ from .Theb import Theb
|
|||
from .TypeGPT import TypeGPT
|
||||
from .Upstage import Upstage
|
||||
from .Vercel import Vercel
|
||||
from .Websim import Websim
|
||||
|
||||
|
|
|
|||
0
g4f/cli/client.py
Executable file → Normal file
0
g4f/cli/client.py
Executable file → Normal file
100
g4f/models.py
100
g4f/models.py
|
|
@ -12,7 +12,6 @@ from .Provider import (
|
|||
Copilot,
|
||||
DeepInfraChat,
|
||||
Free2GPT,
|
||||
FreeGpt,
|
||||
HuggingSpace,
|
||||
Grok,
|
||||
DeepseekAI_JanusPro7b,
|
||||
|
|
@ -21,13 +20,14 @@ from .Provider import (
|
|||
LambdaChat,
|
||||
OIVSCodeSer2,
|
||||
OIVSCodeSer0501,
|
||||
OperaAria,
|
||||
Startnest,
|
||||
OpenAIFM,
|
||||
PerplexityLabs,
|
||||
PollinationsAI,
|
||||
PollinationsImage,
|
||||
TeachAnything,
|
||||
Together,
|
||||
Websim,
|
||||
WeWordle,
|
||||
Yqcloud,
|
||||
|
||||
|
|
@ -148,11 +148,12 @@ default = Model(
|
|||
Blackbox,
|
||||
Copilot,
|
||||
DeepInfraChat,
|
||||
OperaAria,
|
||||
Startnest,
|
||||
LambdaChat,
|
||||
PollinationsAI,
|
||||
Together,
|
||||
Free2GPT,
|
||||
FreeGpt,
|
||||
Chatai,
|
||||
WeWordle,
|
||||
OpenaiChat,
|
||||
|
|
@ -166,9 +167,11 @@ default_vision = VisionModel(
|
|||
best_provider = IterListProvider([
|
||||
Blackbox,
|
||||
DeepInfraChat,
|
||||
OIVSCodeSer2,
|
||||
OIVSCodeSer0501,
|
||||
OIVSCodeSer2,
|
||||
PollinationsAI,
|
||||
OperaAria,
|
||||
Startnest,
|
||||
Together,
|
||||
HuggingSpace,
|
||||
GeminiPro,
|
||||
|
|
@ -196,7 +199,7 @@ gpt_4o = VisionModel(
|
|||
gpt_4o_mini = Model(
|
||||
name = 'gpt-4o-mini',
|
||||
base_provider = 'OpenAI',
|
||||
best_provider = IterListProvider([Blackbox, OIVSCodeSer2, PollinationsAI, Chatai, OpenaiChat])
|
||||
best_provider = IterListProvider([Blackbox, PollinationsAI, Chatai, OIVSCodeSer2, Startnest, OpenaiChat])
|
||||
)
|
||||
|
||||
gpt_4o_mini_audio = AudioModel(
|
||||
|
|
@ -398,12 +401,6 @@ mixtral_8x7b = Model(
|
|||
best_provider = Together
|
||||
)
|
||||
|
||||
mixtral_8x22b = Model(
|
||||
name = "mixtral-8x22b",
|
||||
base_provider = "Mistral AI",
|
||||
best_provider = DeepInfraChat
|
||||
)
|
||||
|
||||
mistral_nemo = Model(
|
||||
name = "mistral-nemo",
|
||||
base_provider = "Mistral AI",
|
||||
|
|
@ -413,13 +410,13 @@ mistral_nemo = Model(
|
|||
mistral_small_24b = Model(
|
||||
name = "mistral-small-24b",
|
||||
base_provider = "Mistral AI",
|
||||
best_provider = IterListProvider([DeepInfraChat, Together])
|
||||
best_provider = Together
|
||||
)
|
||||
|
||||
mistral_small_3_1_24b = Model(
|
||||
name = "mistral-small-3.1-24b",
|
||||
base_provider = "Mistral AI",
|
||||
best_provider = PollinationsAI
|
||||
best_provider = IterListProvider([DeepInfraChat, PollinationsAI])
|
||||
)
|
||||
|
||||
### NousResearch ###
|
||||
|
|
@ -481,13 +478,13 @@ gemini = Model(
|
|||
gemini_1_5_flash = Model(
|
||||
name = 'gemini-1.5-flash',
|
||||
base_provider = 'Google',
|
||||
best_provider = IterListProvider([Free2GPT, FreeGpt, TeachAnything, Websim])
|
||||
best_provider = IterListProvider([Free2GPT, TeachAnything])
|
||||
)
|
||||
|
||||
gemini_1_5_pro = Model(
|
||||
name = 'gemini-1.5-pro',
|
||||
base_provider = 'Google',
|
||||
best_provider = IterListProvider([Free2GPT, FreeGpt, TeachAnything, Websim])
|
||||
best_provider = IterListProvider([Free2GPT, TeachAnything])
|
||||
)
|
||||
|
||||
# gemini-2.0
|
||||
|
|
@ -522,7 +519,34 @@ gemini_2_5_pro = Model(
|
|||
best_provider = IterListProvider([Gemini])
|
||||
)
|
||||
|
||||
# codegemma
|
||||
codegemma_7b = Model(
|
||||
name = 'codegemma-7b',
|
||||
base_provider = 'Google',
|
||||
best_provider = DeepInfraChat
|
||||
)
|
||||
|
||||
# gemma
|
||||
gemma_2b = Model(
|
||||
name = 'gemma-2b',
|
||||
base_provider = 'Google',
|
||||
best_provider = Together
|
||||
)
|
||||
|
||||
# gemma-1
|
||||
gemma_1_1_7b = Model(
|
||||
name = 'gemma-1.1-7b',
|
||||
base_provider = 'Google',
|
||||
best_provider = DeepInfraChat
|
||||
)
|
||||
|
||||
# gemma-2
|
||||
gemma_2_9b = Model(
|
||||
name = 'gemma-2-9b',
|
||||
base_provider = 'Google',
|
||||
best_provider = DeepInfraChat
|
||||
)
|
||||
|
||||
gemma_2_27b = Model(
|
||||
name = 'gemma-2-27b',
|
||||
base_provider = 'Google',
|
||||
|
|
@ -530,6 +554,12 @@ gemma_2_27b = Model(
|
|||
)
|
||||
|
||||
# gemma-3
|
||||
gemma_3_4b = Model(
|
||||
name = 'gemma-3-4b',
|
||||
base_provider = 'Google',
|
||||
best_provider = DeepInfraChat
|
||||
)
|
||||
|
||||
gemma_3_12b = Model(
|
||||
name = 'gemma-3-12b',
|
||||
base_provider = 'Google',
|
||||
|
|
@ -539,7 +569,13 @@ gemma_3_12b = Model(
|
|||
gemma_3_27b = Model(
|
||||
name = 'gemma-3-27b',
|
||||
base_provider = 'Google',
|
||||
best_provider = DeepInfraChat
|
||||
best_provider = IterListProvider([DeepInfraChat, Together])
|
||||
)
|
||||
|
||||
gemma_3n_e4b = Model(
|
||||
name = 'gemma-3n-e4b',
|
||||
base_provider = 'Google',
|
||||
best_provider = Together
|
||||
)
|
||||
|
||||
### Blackbox AI ###
|
||||
|
|
@ -586,7 +622,7 @@ qwen_1_5_7b = Model(
|
|||
qwen_2_72b = Model(
|
||||
name = 'qwen-2-72b',
|
||||
base_provider = 'Qwen',
|
||||
best_provider = IterListProvider([DeepInfraChat, HuggingSpace, Together])
|
||||
best_provider = IterListProvider([HuggingSpace, Together])
|
||||
)
|
||||
|
||||
qwen_2_vl_7b = VisionModel(
|
||||
|
|
@ -654,7 +690,7 @@ qwen_3_235b = Model(
|
|||
qwen_3_32b = Model(
|
||||
name = 'qwen-3-32b',
|
||||
base_provider = 'Qwen',
|
||||
best_provider = IterListProvider([DeepInfraChat, LambdaChat, HuggingSpace])
|
||||
best_provider = IterListProvider([DeepInfraChat, LambdaChat, Together, HuggingSpace])
|
||||
)
|
||||
|
||||
qwen_3_30b = Model(
|
||||
|
|
@ -759,6 +795,12 @@ deepseek_v3_0324 = Model(
|
|||
best_provider = IterListProvider([DeepInfraChat, LambdaChat, PollinationsAI])
|
||||
)
|
||||
|
||||
deepseek_v3_0324_turbo = Model(
|
||||
name = 'deepseek-v3-0324-turbo',
|
||||
base_provider = 'DeepSeek',
|
||||
best_provider = DeepInfraChat
|
||||
)
|
||||
|
||||
# deepseek-r1-0528
|
||||
deepseek_r1_0528 = Model(
|
||||
name = 'deepseek-r1-0528',
|
||||
|
|
@ -766,6 +808,12 @@ deepseek_r1_0528 = Model(
|
|||
best_provider = IterListProvider([DeepInfraChat, LambdaChat])
|
||||
)
|
||||
|
||||
deepseek_r1_0528_turbo = Model(
|
||||
name = 'deepseek-r1-0528-turbo',
|
||||
base_provider = 'DeepSeek',
|
||||
best_provider = DeepInfraChat
|
||||
)
|
||||
|
||||
# janus
|
||||
janus_pro_7b = VisionModel(
|
||||
name = DeepseekAI_JanusPro7b.default_model,
|
||||
|
|
@ -871,6 +919,13 @@ lfm_40b = Model(
|
|||
best_provider = LambdaChat
|
||||
)
|
||||
|
||||
### Opera ###
|
||||
aria = Model(
|
||||
name = "aria",
|
||||
base_provider = "Opera",
|
||||
best_provider = OperaAria
|
||||
)
|
||||
|
||||
### Uncensored AI ###
|
||||
evil = Model(
|
||||
name = 'evil',
|
||||
|
|
@ -878,6 +933,7 @@ evil = Model(
|
|||
best_provider = PollinationsAI
|
||||
)
|
||||
|
||||
### Stability AI ###
|
||||
sdxl_turbo = ImageModel(
|
||||
name = 'sdxl-turbo',
|
||||
base_provider = 'Stability AI',
|
||||
|
|
@ -894,7 +950,7 @@ sd_3_5_large = ImageModel(
|
|||
flux = ImageModel(
|
||||
name = 'flux',
|
||||
base_provider = 'Black Forest Labs',
|
||||
best_provider = IterListProvider([HuggingFaceMedia, PollinationsImage, Websim, Together, HuggingSpace])
|
||||
best_provider = IterListProvider([HuggingFaceMedia, PollinationsImage, Together, HuggingSpace])
|
||||
)
|
||||
|
||||
flux_pro = ImageModel(
|
||||
|
|
@ -951,6 +1007,12 @@ flux_kontext_pro = ImageModel(
|
|||
best_provider = Together
|
||||
)
|
||||
|
||||
flux_kontext_dev = ImageModel(
|
||||
name = 'flux-kontext-dev',
|
||||
base_provider = 'Black Forest Labs',
|
||||
best_provider = Together
|
||||
)
|
||||
|
||||
class ModelUtils:
|
||||
"""
|
||||
Utility class for mapping string identifiers to Model instances.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -10,8 +10,8 @@ from ..providers.retry_provider import IterListProvider
|
|||
from ..Provider.needs_auth import OpenaiChat, CopilotAccount
|
||||
from ..Provider.hf_space import HuggingSpace
|
||||
from ..Provider import Cloudflare, Gemini, GeminiPro, Grok, DeepSeekAPI, PerplexityLabs, LambdaChat, PollinationsAI, PuterJS
|
||||
from ..Provider import Microsoft_Phi_4_Multimodal, DeepInfraChat, Blackbox, OIVSCodeSer2, OIVSCodeSer0501, TeachAnything
|
||||
from ..Provider import Together, WeWordle, Yqcloud, Chatai, Free2GPT, ImageLabs, LegacyLMArena, LMArenaBeta
|
||||
from ..Provider import Microsoft_Phi_4_Multimodal, DeepInfraChat, Blackbox, OIVSCodeSer0501, OIVSCodeSer2, TeachAnything, OperaAria, Startnest
|
||||
from ..Provider import Together, WeWordle, Yqcloud, Chatai, ImageLabs, LegacyLMArena, LMArenaBeta, Free2GPT
|
||||
from ..Provider import EdgeTTS, gTTS, MarkItDown, OpenAIFM, Video
|
||||
from ..Provider import HarProvider, HuggingFace, HuggingFaceMedia
|
||||
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
|
||||
|
|
@ -25,7 +25,7 @@ PROVIERS_LIST_1 = [
|
|||
OIVSCodeSer2, OIVSCodeSer0501, TeachAnything, WeWordle, Yqcloud, Chatai, Free2GPT, ImageLabs,
|
||||
# Has lazy loading model lists
|
||||
PollinationsAI, HarProvider, LegacyLMArena, LMArenaBeta, LambdaChat, DeepInfraChat,
|
||||
HuggingSpace, HuggingFace, HuggingFaceMedia, GeminiPro, Together, PuterJS
|
||||
HuggingSpace, HuggingFace, HuggingFaceMedia, GeminiPro, Together, PuterJS, OperaAria, Startnest
|
||||
]
|
||||
|
||||
PROVIERS_LIST_2 = [
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@ from urllib.parse import urlparse, quote_plus
|
|||
from datetime import date
|
||||
import asyncio
|
||||
|
||||
# Optional dependencies
|
||||
# Optional dependencies using the new 'ddgs' package name
|
||||
try:
|
||||
from duckduckgo_search import DDGS
|
||||
from duckduckgo_search.exceptions import DuckDuckGoSearchException
|
||||
from ddgs import DDGS, DDGSError
|
||||
from bs4 import BeautifulSoup
|
||||
ddgs = DDGS()
|
||||
has_requirements = True
|
||||
except ImportError:
|
||||
has_requirements = False
|
||||
|
|
@ -95,18 +93,8 @@ class SearchResultEntry(JsonMixin):
|
|||
def scrape_text(html: str, max_words: Optional[int] = None, add_source: bool = True, count_images: int = 2) -> Iterator[str]:
|
||||
"""
|
||||
Parses the provided HTML and yields text fragments.
|
||||
|
||||
Args:
|
||||
html (str): HTML content to scrape.
|
||||
max_words (int, optional): Maximum words allowed. Defaults to None.
|
||||
add_source (bool): Whether to append source link at the end.
|
||||
count_images (int): Maximum number of images to include.
|
||||
|
||||
Yields:
|
||||
str: Text or markdown image links extracted from the HTML.
|
||||
"""
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
# Try to narrow the parsing scope using common selectors.
|
||||
for selector in [
|
||||
"main", ".main-content-wrapper", ".main-content", ".emt-container-inner",
|
||||
".content-wrapper", "#content", "#mainContent",
|
||||
|
|
@ -116,7 +104,6 @@ def scrape_text(html: str, max_words: Optional[int] = None, add_source: bool = T
|
|||
soup = selected
|
||||
break
|
||||
|
||||
# Remove unwanted elements.
|
||||
for remove_selector in [".c-globalDisclosure"]:
|
||||
unwanted = soup.select_one(remove_selector)
|
||||
if unwanted:
|
||||
|
|
@ -126,13 +113,10 @@ def scrape_text(html: str, max_words: Optional[int] = None, add_source: bool = T
|
|||
image_link_selector = f"a:has({image_selector})"
|
||||
seen_texts = []
|
||||
|
||||
# Iterate over paragraphs and other elements.
|
||||
for element in soup.select(f"h1, h2, h3, h4, h5, h6, p, pre, table:not(:has(p)), ul:not(:has(p)), {image_link_selector}"):
|
||||
# Process images if available and allowed.
|
||||
if count_images > 0:
|
||||
image = element.select_one(image_selector)
|
||||
if image:
|
||||
# Use the element's title attribute if available, otherwise use its text.
|
||||
title = str(element.get("title", element.text))
|
||||
if title:
|
||||
yield f"!{format_link(image['src'], title)}\n"
|
||||
|
|
@ -141,7 +125,6 @@ def scrape_text(html: str, max_words: Optional[int] = None, add_source: bool = T
|
|||
count_images -= 1
|
||||
continue
|
||||
|
||||
# Split the element text into lines and yield non-duplicate lines.
|
||||
for line in element.get_text(" ").splitlines():
|
||||
words = [word for word in line.split() if word]
|
||||
if not words:
|
||||
|
|
@ -156,7 +139,6 @@ def scrape_text(html: str, max_words: Optional[int] = None, add_source: bool = T
|
|||
yield joined_line + "\n"
|
||||
seen_texts.append(joined_line)
|
||||
|
||||
# Add a canonical link as source info if requested.
|
||||
if add_source:
|
||||
canonical_link = soup.find("link", rel="canonical")
|
||||
if canonical_link and "href" in canonical_link.attrs:
|
||||
|
|
@ -167,21 +149,11 @@ def scrape_text(html: str, max_words: Optional[int] = None, add_source: bool = T
|
|||
async def fetch_and_scrape(session: ClientSession, url: str, max_words: Optional[int] = None, add_source: bool = False) -> str:
|
||||
"""
|
||||
Fetches a URL and returns the scraped text, using caching to avoid redundant downloads.
|
||||
|
||||
Args:
|
||||
session (ClientSession): An aiohttp client session.
|
||||
url (str): URL to fetch.
|
||||
max_words (int, optional): Maximum words for the scraped text.
|
||||
add_source (bool): Whether to append source link.
|
||||
|
||||
Returns:
|
||||
str: The scraped text or an empty string in case of errors.
|
||||
"""
|
||||
try:
|
||||
cache_dir: Path = Path(get_cookies_dir()) / ".scrape_cache" / "fetch_and_scrape"
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
md5_hash = hashlib.md5(url.encode(errors="ignore")).hexdigest()
|
||||
# Build cache filename using a portion of the URL and current date.
|
||||
cache_file = cache_dir / f"{quote_plus(url.split('?')[0].split('//')[1].replace('/', ' ')[:48])}.{date.today()}.{md5_hash[:16]}.cache"
|
||||
if cache_file.exists():
|
||||
return cache_file.read_text()
|
||||
|
|
@ -205,30 +177,14 @@ async def search(
|
|||
add_text: bool = True,
|
||||
timeout: int = 5,
|
||||
region: str = "wt-wt",
|
||||
provider: str = "DDG" # Default fallback to DuckDuckGo
|
||||
provider: str = "DDG"
|
||||
) -> SearchResults:
|
||||
"""
|
||||
Performs a web search and returns search results.
|
||||
|
||||
Args:
|
||||
query (str): The search query.
|
||||
max_results (int): Maximum number of results.
|
||||
max_words (int): Maximum words for textual results.
|
||||
backend (str): Backend type.
|
||||
add_text (bool): Whether to fetch and add full text to each result.
|
||||
timeout (int): Timeout for HTTP requests.
|
||||
region (str): Region parameter for the search engine.
|
||||
provider (str): The search provider to use.
|
||||
|
||||
Returns:
|
||||
SearchResults: The collection of search results and used words.
|
||||
"""
|
||||
# If using SearXNG provider.
|
||||
if provider == "SearXNG":
|
||||
from ..Provider.SearXNG import SearXNG
|
||||
|
||||
debug.log(f"[SearXNG] Using local container for query: {query}")
|
||||
|
||||
results_texts = []
|
||||
async for chunk in SearXNG.create_async_generator(
|
||||
"SearXNG",
|
||||
|
|
@ -239,9 +195,7 @@ async def search(
|
|||
):
|
||||
if isinstance(chunk, str):
|
||||
results_texts.append(chunk)
|
||||
|
||||
used_words = sum(text.count(" ") for text in results_texts)
|
||||
|
||||
return SearchResults([
|
||||
SearchResultEntry(
|
||||
title=f"Result {i + 1}",
|
||||
|
|
@ -251,37 +205,34 @@ async def search(
|
|||
) for i, text in enumerate(results_texts)
|
||||
], used_words=used_words)
|
||||
|
||||
# -------------------------
|
||||
# Default: DuckDuckGo logic
|
||||
# -------------------------
|
||||
debug.log(f"[DuckDuckGo] Using local container for query: {query}")
|
||||
|
||||
if not has_requirements:
|
||||
raise MissingRequirementsError('Install "duckduckgo-search" and "beautifulsoup4" | pip install -U g4f[search]')
|
||||
raise MissingRequirementsError('Install "ddgs" and "beautifulsoup4" | pip install -U g4f[search]')
|
||||
|
||||
results: List[SearchResultEntry] = []
|
||||
for result in ddgs.text(
|
||||
query,
|
||||
region=region,
|
||||
safesearch="moderate",
|
||||
timelimit="y",
|
||||
max_results=max_results,
|
||||
backend=backend,
|
||||
):
|
||||
if ".google." in result["href"]:
|
||||
continue
|
||||
results.append(SearchResultEntry(
|
||||
title=result["title"],
|
||||
url=result["href"],
|
||||
snippet=result["body"]
|
||||
))
|
||||
# Use the new DDGS() context manager style
|
||||
async with DDGS() as ddgs:
|
||||
async for result in ddgs.text(
|
||||
query,
|
||||
region=region,
|
||||
safesearch="moderate",
|
||||
timelimit="y",
|
||||
max_results=max_results,
|
||||
backend=backend,
|
||||
):
|
||||
if ".google." in result["href"]:
|
||||
continue
|
||||
results.append(SearchResultEntry(
|
||||
title=result["title"],
|
||||
url=result["href"],
|
||||
snippet=result["body"]
|
||||
))
|
||||
|
||||
# Optionally add full text for each result.
|
||||
if add_text:
|
||||
tasks = []
|
||||
async with ClientSession(timeout=ClientTimeout(timeout)) as session:
|
||||
for entry in results:
|
||||
# Divide available words among results
|
||||
tasks.append(fetch_and_scrape(session, entry.url, int(max_words / (max_results - 1)), False))
|
||||
texts = await asyncio.gather(*tasks)
|
||||
|
||||
|
|
@ -291,7 +242,6 @@ async def search(
|
|||
for i, entry in enumerate(results):
|
||||
if add_text:
|
||||
entry.text = texts[i]
|
||||
# Deduct word counts for title and text/snippet.
|
||||
left_words -= entry.title.count(" ") + 5
|
||||
if entry.text:
|
||||
left_words -= entry.text.count(" ")
|
||||
|
|
@ -312,31 +262,19 @@ async def do_search(
|
|||
) -> tuple[str, Optional[Sources]]:
|
||||
"""
|
||||
Combines search results with the user prompt, using caching for improved efficiency.
|
||||
|
||||
Args:
|
||||
prompt (str): The user prompt.
|
||||
query (str, optional): The search query. If None the first line of prompt is used.
|
||||
instructions (str): Additional instructions to append.
|
||||
**kwargs: Additional parameters for the search.
|
||||
|
||||
Returns:
|
||||
tuple[str, Optional[Sources]]: A tuple containing the new prompt with search results and the sources.
|
||||
"""
|
||||
if not isinstance(prompt, str):
|
||||
return prompt, None
|
||||
|
||||
# If the prompt already includes the instructions, do not perform a search.
|
||||
if instructions and instructions in prompt:
|
||||
return prompt, None
|
||||
|
||||
if prompt.startswith("##") and query is None:
|
||||
return prompt, None
|
||||
|
||||
# Use the first line of the prompt as the query if not provided.
|
||||
if query is None:
|
||||
query = prompt.strip().splitlines()[0]
|
||||
|
||||
# Prepare a cache key.
|
||||
json_bytes = json.dumps({"query": query, **kwargs}, sort_keys=True).encode(errors="ignore")
|
||||
md5_hash = hashlib.md5(json_bytes).hexdigest()
|
||||
cache_dir: Path = Path(get_cookies_dir()) / ".scrape_cache" / "web_search" / f"{date.today()}"
|
||||
|
|
@ -344,7 +282,6 @@ async def do_search(
|
|||
cache_file = cache_dir / f"{quote_plus(query[:20])}.{md5_hash}.cache"
|
||||
|
||||
search_results: Optional[SearchResults] = None
|
||||
# Load cached search results if available.
|
||||
if cache_file.exists():
|
||||
with cache_file.open("r") as f:
|
||||
try:
|
||||
|
|
@ -352,7 +289,6 @@ async def do_search(
|
|||
except json.JSONDecodeError:
|
||||
search_results = None
|
||||
|
||||
# Otherwise perform the search.
|
||||
if search_results is None:
|
||||
search_results = await search(query, **kwargs)
|
||||
if search_results.results:
|
||||
|
|
@ -360,20 +296,9 @@ async def do_search(
|
|||
f.write(json.dumps(search_results.get_dict()))
|
||||
|
||||
if instructions:
|
||||
new_prompt = f"""
|
||||
{search_results}
|
||||
|
||||
Instruction: {instructions}
|
||||
|
||||
User request:
|
||||
{prompt}
|
||||
"""
|
||||
new_prompt = f"{search_results}\n\nInstruction: {instructions}\n\nUser request:\n{prompt}"
|
||||
else:
|
||||
new_prompt = f"""
|
||||
{search_results}
|
||||
|
||||
{prompt}
|
||||
"""
|
||||
new_prompt = f"{search_results}\n\n{prompt}"
|
||||
|
||||
debug.log(f"Web search: '{query.strip()[:50]}...'")
|
||||
debug.log(f"with {len(search_results.results)} Results {search_results.used_words} Words")
|
||||
|
|
@ -382,19 +307,12 @@ User request:
|
|||
def get_search_message(prompt: str, raise_search_exceptions: bool = False, **kwargs) -> str:
|
||||
"""
|
||||
Synchronously obtains the search message by wrapping the async search call.
|
||||
|
||||
Args:
|
||||
prompt (str): The original prompt.
|
||||
raise_search_exceptions (bool): Whether to propagate search exceptions.
|
||||
**kwargs: Additional search parameters.
|
||||
|
||||
Returns:
|
||||
str: The new prompt including search results.
|
||||
"""
|
||||
try:
|
||||
result, _ = asyncio.run(do_search(prompt, **kwargs))
|
||||
return result
|
||||
except (DuckDuckGoSearchException, MissingRequirementsError) as e:
|
||||
# Use the new DDGSError exception
|
||||
except (DDGSError, MissingRequirementsError) as e:
|
||||
if raise_search_exceptions:
|
||||
raise e
|
||||
debug.error(f"Couldn't do web search: {e.__class__.__name__}: {e}")
|
||||
|
|
|
|||
|
|
@ -20,4 +20,4 @@ nodriver
|
|||
python-multipart
|
||||
markitdown[all]
|
||||
a2wsgi
|
||||
python-dotenv
|
||||
python-dotenv
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue