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:
kqlio67 2025-07-11 01:50:59 +00:00 committed by GitHub
parent 85bd2cbf28
commit 7965487830
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 813 additions and 219 deletions

View file

@ -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}')

View file

@ -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

View file

@ -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
View 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
View 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}&timestamp={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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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
View file

View file

@ -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

View file

@ -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 = [

View file

@ -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}")

View file

@ -20,4 +20,4 @@ nodriver
python-multipart
markitdown[all]
a2wsgi
python-dotenv
python-dotenv