feat: add Qwen Qwen-3 provider and update audio/media handling

- Introduce `Qwen_Qwen_3` provider in `g4f/Provider/hf_space/Qwen_Qwen_3.py`
- Register Qwen_Qwen_3 in `g4f/Provider/hf_space/__init__.py` and add it to `HuggingSpace`
- Update `MarkItDown` in `g4f/Provider/audio/MarkItDown.py` to accept and forward `llm_client` and `llm_model` kwargs; add async handling for `text_content`
- Modify audio route in `g4f/api/__init__.py` to pass `llm_client` for MarkItDown and set `modalities` only for other providers
- Adjust `OpenaiChat` (needs_auth) to merge media for upload and check for media presence before requesting images
- Change `get_tempfile` in `g4f/tools/files.py` to determine suffix from file extension using `os.path.splitext`
- Refactor provider listing and model mapping in `AnyProvider.get_models()` (g4f/providers/any_provider.py) to update provider order, support new `HarProvider`, initialize attributes, and guard against model_aliases being None
- Ensure `AnyProvider.create_async_generator` calls `get_models` before working with providers
This commit is contained in:
hlohaus 2025-05-01 10:22:12 +02:00
parent c9e1bd21fb
commit ab5a089b7e
7 changed files with 250 additions and 96 deletions

View file

@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import os import os
import asyncio
from typing import Any
try: try:
from markitdown import MarkItDown as MaItDo, StreamInfo from markitdown import MarkItDown as MaItDo, StreamInfo
@ -21,6 +23,7 @@ class MarkItDown(AsyncGeneratorProvider, ProviderModelMixin):
model: str, model: str,
messages: Messages, messages: Messages,
media: MediaListType = None, media: MediaListType = None,
llm_client: Any = None,
**kwargs **kwargs
) -> AsyncResult: ) -> AsyncResult:
if media is None: if media is None:
@ -31,11 +34,28 @@ class MarkItDown(AsyncGeneratorProvider, ProviderModelMixin):
for file, filename in media: for file, filename in media:
text = None text = None
try: try:
text = md.convert(file, stream_info=StreamInfo(filename=filename) if filename else None).text_content result = md.convert(
file,
stream_info=StreamInfo(filename=filename) if filename else None,
llm_client=llm_client,
llm_model=model
)
if asyncio.iscoroutine(result.text_content):
text = await result.text_content
else:
text = result.text_content
except TypeError: except TypeError:
copyfile = get_tempfile(file, filename) copyfile = get_tempfile(file, filename)
try: try:
text = md.convert(copyfile).text_content result = md.convert(
copyfile,
llm_client=llm_client,
llm_model=model
)
if asyncio.iscoroutine(result.text_content):
text = await result.text_content
else:
text = result.text_content
finally: finally:
os.remove(copyfile) os.remove(copyfile)
text = text.split("### Audio Transcript:\n")[-1] text = text.split("### Audio Transcript:\n")[-1]

View file

@ -0,0 +1,122 @@
from __future__ import annotations
import aiohttp
import json
import uuid
from ...typing import AsyncResult, Messages
from ...providers.response import Reasoning, JsonConversation
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
from ..helper import get_last_user_message
from ... import debug
class Qwen_Qwen_3(AsyncGeneratorProvider, ProviderModelMixin):
label = "Qwen Qwen-3"
url = "https://qwen-qwen3-demo.hf.space"
api_endpoint = "https://qwen-qwen3-demo.hf.space/gradio_api/queue/join?__theme=system"
working = True
supports_stream = True
supports_system_message = True
default_model = "qwen3-235b-a22b"
models = {
default_model,
"qwen3-32b",
"qwen3-30b-a3b",
"qwen3-14b",
"qwen3-8b",
"qwen3-4b",
"qwen3-1.7b",
"qwen3-0.6b",
}
model_aliases = {model: model for model in models}
@classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
conversation: JsonConversation = None,
thinking_budget: int = 38,
**kwargs
) -> AsyncResult:
if conversation is None:
conversation = JsonConversation(session_hash=str(uuid.uuid4()).replace('-', ''))
headers_join = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Referer': f'{cls.url}/?__theme=system',
'content-type': 'application/json',
'Origin': cls.url,
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
}
sys_prompt = "\n".join([message['content'] for message in messages if message['role'] == 'system'])
sys_prompt = sys_prompt if sys_prompt else "You are a helpful and harmless assistant."
payload_join = {"data":[
get_last_user_message(messages),
{"thinking_budget": thinking_budget, "model": cls.get_model(model), "sys_prompt": sys_prompt}, None, None],
"event_data":None,"fn_index":13,"trigger_id":31,"session_hash":conversation.session_hash
}
async with aiohttp.ClientSession() as session:
# Send join request
async with session.post(cls.api_endpoint, headers=headers_join, json=payload_join) as response:
(await response.json())['event_id']
# Prepare data stream request
url_data = f'{cls.url}/gradio_api/queue/data'
headers_data = {
'Accept': 'text/event-stream',
'Accept-Language': 'en-US,en;q=0.5',
'Referer': f'{cls.url}/?__theme=system',
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0',
}
params_data = {
'session_hash': conversation.session_hash,
}
# Send data stream request
async with session.get(url_data, headers=headers_data, params=params_data) as response:
is_thinking = False
async for line in response.content:
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data: '):
try:
json_data = json.loads(decoded_line[6:])
# Look for generation stages
if json_data.get('msg') == 'process_generating':
if 'output' in json_data and 'data' in json_data['output'] and len(json_data['output']['data']) > 5:
updates = json_data['output']['data'][5]
for update in updates:
if isinstance(update[2], dict):
if update[2].get('type') == 'tool':
yield Reasoning(update[2].get('content'), status=update[2].get('options', {}).get('title'))
is_thinking = True
elif isinstance(update, list) and isinstance(update[1], list) and len(update[1]) > 4:
if update[1][4] == "content":
yield Reasoning(update[2]) if is_thinking else update[2]
elif update[1][4] == "options":
if update[2] != "done":
yield Reasoning(status=update[2])
is_thinking = False
# Check for completion
if json_data.get('msg') == 'process_completed':
break
except json.JSONDecodeError:
debug.log("Could not parse JSON:", decoded_line)

View file

@ -16,6 +16,7 @@ from .Qwen_Qwen_2_5 import Qwen_Qwen_2_5
from .Qwen_Qwen_2_5M import Qwen_Qwen_2_5M from .Qwen_Qwen_2_5M import Qwen_Qwen_2_5M
from .Qwen_Qwen_2_5_Max import Qwen_Qwen_2_5_Max from .Qwen_Qwen_2_5_Max import Qwen_Qwen_2_5_Max
from .Qwen_Qwen_2_72B import Qwen_Qwen_2_72B from .Qwen_Qwen_2_72B import Qwen_Qwen_2_72B
from .Qwen_Qwen_3 import Qwen_Qwen_3
from .StabilityAI_SD35Large import StabilityAI_SD35Large from .StabilityAI_SD35Large import StabilityAI_SD35Large
from .Voodoohop_Flux1Schnell import Voodoohop_Flux1Schnell from .Voodoohop_Flux1Schnell import Voodoohop_Flux1Schnell
@ -38,6 +39,7 @@ class HuggingSpace(AsyncGeneratorProvider, ProviderModelMixin):
Qwen_Qwen_2_5M, Qwen_Qwen_2_5M,
Qwen_Qwen_2_5_Max, Qwen_Qwen_2_5_Max,
Qwen_Qwen_2_72B, Qwen_Qwen_2_72B,
Qwen_Qwen_3,
StabilityAI_SD35Large, StabilityAI_SD35Large,
Voodoohop_Flux1Schnell, Voodoohop_Flux1Schnell,
] ]

View file

@ -337,7 +337,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
timeout=timeout timeout=timeout
) as session: ) as session:
image_requests = None image_requests = None
if not cls.needs_auth: media = merge_media(media, messages)
if not cls.needs_auth and not media:
if cls._headers is None: if cls._headers is None:
cls._create_request_args(cls._cookies) cls._create_request_args(cls._cookies)
async with session.get(cls.url, headers=INIT_HEADERS) as response: async with session.get(cls.url, headers=INIT_HEADERS) as response:
@ -352,7 +353,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
cls._update_request_args(auth_result, session) cls._update_request_args(auth_result, session)
await raise_for_status(response) await raise_for_status(response)
try: try:
image_requests = await cls.upload_images(session, auth_result, merge_media(media, messages)) image_requests = await cls.upload_images(session, auth_result, media)
except Exception as e: except Exception as e:
debug.error("OpenaiChat: Upload image failed") debug.error("OpenaiChat: Upload image failed")
debug.error(e) debug.error(e)

View file

@ -508,13 +508,19 @@ class Api:
provider: Annotated[Optional[str], Form()] = None, provider: Annotated[Optional[str], Form()] = None,
prompt: Annotated[Optional[str], Form()] = "Transcribe this audio" prompt: Annotated[Optional[str], Form()] = "Transcribe this audio"
): ):
provider = provider if path_provider is None else path_provider
kwargs = {"modalities": ["text"]}
if provider == "MarkItDown":
kwargs = {
"llm_client": self.client,
}
try: try:
response = await self.client.chat.completions.create( response = await self.client.chat.completions.create(
messages=prompt, messages=prompt,
model=model, model=model,
provider=provider if path_provider is None else path_provider, provider=provider,
media=[[file.file, file.filename]], media=[[file.file, file.filename]],
modalities=["text"] **kwargs
) )
return {"text": response.choices[0].message.content, "model": response.model, "provider": response.provider} return {"text": response.choices[0].message.content, "model": response.model, "provider": response.provider}
except (ModelNotFoundError, ProviderNotFoundError) as e: except (ModelNotFoundError, ProviderNotFoundError) as e:

View file

@ -6,12 +6,11 @@ from ..providers.retry_provider import IterListProvider
from ..image import is_data_an_audio from ..image import is_data_an_audio
from ..providers.response import JsonConversation, ProviderInfo from ..providers.response import JsonConversation, ProviderInfo
from ..Provider.needs_auth import OpenaiChat, CopilotAccount from ..Provider.needs_auth import OpenaiChat, CopilotAccount
from ..Provider.hf import HuggingFace, HuggingFaceMedia
from ..Provider.hf_space import HuggingSpace from ..Provider.hf_space import HuggingSpace
from .. import Provider from .. import Provider
from .. import models from .. import models
from ..Provider import Cloudflare, LMArenaProvider, Gemini, Grok, DeepSeekAPI, PerplexityLabs, LambdaChat, PollinationsAI, FreeRouter from ..Provider import Cloudflare, Gemini, Grok, DeepSeekAPI, PerplexityLabs, LambdaChat, PollinationsAI, FreeRouter
from ..Provider import Microsoft_Phi_4, DeepInfraChat, Blackbox, EdgeTTS, gTTS, MarkItDown from ..Provider import Microsoft_Phi_4, DeepInfraChat, Blackbox, EdgeTTS, gTTS, MarkItDown, HarProvider
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin): class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin):
@ -20,90 +19,93 @@ class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin):
@classmethod @classmethod
def get_models(cls, ignored: list[str] = []) -> list[str]: def get_models(cls, ignored: list[str] = []) -> list[str]:
cls.audio_models = {} if not cls.models:
cls.image_models = [] cls.audio_models = {}
cls.vision_models = [] cls.image_models = []
cls.video_models = [] cls.vision_models = []
model_with_providers = { cls.video_models = []
model: [ model_with_providers = {
provider for provider in providers model: [
if provider.working and getattr(provider, "parent", provider.__name__) not in ignored provider for provider in providers
] for model, (_, providers) in models.__models__.items() if provider.working and getattr(provider, "parent", provider.__name__) not in ignored
} ] for model, (_, providers) in models.__models__.items()
model_with_providers = { }
model: providers for model, providers in model_with_providers.items() model_with_providers = {
if providers model: providers for model, providers in model_with_providers.items()
} if providers
cls.models_count = { }
model: len(providers) for model, providers in model_with_providers.items() if len(providers) > 1 cls.models_count = {
} model: len(providers) for model, providers in model_with_providers.items() if len(providers) > 1
all_models = [cls.default_model] + list(model_with_providers.keys()) }
for provider in [OpenaiChat, PollinationsAI, HuggingSpace, Cloudflare, PerplexityLabs, Gemini, Grok]: all_models = [cls.default_model] + list(model_with_providers.keys())
if not provider.working or getattr(provider, "parent", provider.__name__) in ignored: for provider in [OpenaiChat, PollinationsAI, HuggingSpace, Cloudflare, PerplexityLabs, Gemini, Grok]:
continue if not provider.working or getattr(provider, "parent", provider.__name__) in ignored:
if provider == PollinationsAI: continue
all_models.extend([f"{provider.__name__}:{model}" for model in provider.get_models() if model not in all_models]) if provider == PollinationsAI:
cls.audio_models.update({f"{provider.__name__}:{model}": [] for model in provider.get_models() if model in provider.audio_models}) all_models.extend([f"{provider.__name__}:{model}" for model in provider.get_models() if model not in all_models])
cls.image_models.extend([f"{provider.__name__}:{model}" for model in provider.get_models() if model in provider.image_models]) cls.audio_models.update({f"{provider.__name__}:{model}": [] for model in provider.get_models() if model in provider.audio_models})
cls.vision_models.extend([f"{provider.__name__}:{model}" for model in provider.get_models() if model in provider.vision_models]) cls.image_models.extend([f"{provider.__name__}:{model}" for model in provider.get_models() if model in provider.image_models])
else: cls.vision_models.extend([f"{provider.__name__}:{model}" for model in provider.get_models() if model in provider.vision_models])
all_models.extend(provider.get_models()) else:
cls.image_models.extend(provider.image_models) all_models.extend(provider.get_models())
cls.vision_models.extend(provider.vision_models) cls.image_models.extend(provider.image_models)
cls.video_models.extend(provider.video_models) cls.vision_models.extend(provider.vision_models)
if CopilotAccount.working and CopilotAccount.parent not in ignored: cls.video_models.extend(provider.video_models)
all_models.extend(list(CopilotAccount.model_aliases.keys())) if CopilotAccount.working and CopilotAccount.parent not in ignored:
if PollinationsAI.working and PollinationsAI.__name__ not in ignored: all_models.extend(list(CopilotAccount.model_aliases.keys()))
all_models.extend(list(PollinationsAI.model_aliases.keys())) if PollinationsAI.working and PollinationsAI.__name__ not in ignored:
def clean_name(name: str) -> str: all_models.extend(list(PollinationsAI.model_aliases.keys()))
return name.split("/")[-1].split(":")[0].lower( def clean_name(name: str) -> str:
).replace("-instruct", "" return name.split("/")[-1].split(":")[0].lower(
).replace("-chat", "" ).replace("-instruct", ""
).replace("-08-2024", "" ).replace("-chat", ""
).replace("-03-2025", "" ).replace("-08-2024", ""
).replace("-20250219", "" ).replace("-03-2025", ""
).replace("-20241022", "" ).replace("-20250219", ""
).replace("-20240904", "" ).replace("-20241022", ""
).replace("-2025-04-16", "" ).replace("-20240904", ""
).replace("-2025-04-14", "" ).replace("-2025-04-16", ""
).replace("-0125", "" ).replace("-2025-04-14", ""
).replace("-2407", "" ).replace("-0125", ""
).replace("-2501", "" ).replace("-2407", ""
).replace("-0324", "" ).replace("-2501", ""
).replace("-2409", "" ).replace("-0324", ""
).replace("-2410", "" ).replace("-2409", ""
).replace("-2411", "" ).replace("-2410", ""
).replace("-1119", "" ).replace("-2411", ""
).replace("-0919", "" ).replace("-1119", ""
).replace("-02-24", "" ).replace("-0919", ""
).replace("-03-25", "" ).replace("-02-24", ""
).replace("-03-26", "" ).replace("-03-25", ""
).replace("-01-21", "" ).replace("-03-26", ""
).replace("-002", "" ).replace("-01-21", ""
).replace(".1-", "-" ).replace("-002", ""
).replace("_", "." ).replace("_", "."
).replace("c4ai-", "" ).replace("c4ai-", ""
).replace("-preview", "" ).replace("-preview", ""
).replace("-experimental", "" ).replace("-experimental", ""
).replace("-v1", "" ).replace("-v1", ""
).replace("-fp8", "" ).replace("-fp8", ""
).replace("-bf16", "" ).replace("-bf16", ""
).replace("-hf", "" ).replace("-hf", ""
).replace("llama3", "llama-3") ).replace("llama3", "llama-3")
for provider in [HuggingFace, HuggingFaceMedia, LMArenaProvider, LambdaChat, DeepInfraChat]: for provider in [HarProvider, LambdaChat, DeepInfraChat]:
if not provider.working or getattr(provider, "parent", provider.__name__) in ignored: if not provider.working or getattr(provider, "parent", provider.__name__) in ignored:
continue continue
model_map = {clean_name(model): model for model in provider.get_models()} model_map = {clean_name(model): model for model in provider.get_models()}
provider.model_aliases.update(model_map) if not provider.model_aliases:
all_models.extend(list(model_map.keys())) provider.model_aliases = {}
cls.image_models.extend([clean_name(model) for model in provider.image_models]) provider.model_aliases.update(model_map)
cls.vision_models.extend([clean_name(model) for model in provider.vision_models]) all_models.extend(list(model_map.keys()))
cls.video_models.extend([clean_name(model) for model in provider.video_models]) cls.image_models.extend([clean_name(model) for model in provider.image_models])
for provider in [Microsoft_Phi_4, PollinationsAI]: cls.vision_models.extend([clean_name(model) for model in provider.vision_models])
if provider.working and getattr(provider, "parent", provider.__name__) not in ignored: cls.video_models.extend([clean_name(model) for model in provider.video_models])
cls.audio_models.update(provider.audio_models) for provider in [Microsoft_Phi_4, PollinationsAI]:
cls.models_count.update({model: all_models.count(model) for model in all_models if all_models.count(model) > cls.models_count.get(model, 0)}) if provider.working and getattr(provider, "parent", provider.__name__) not in ignored:
return list(dict.fromkeys([model if model else cls.default_model for model in all_models])) cls.audio_models.update(provider.audio_models)
cls.models_count.update({model: all_models.count(model) for model in all_models if all_models.count(model) > cls.models_count.get(model, 0)})
cls.models = list(dict.fromkeys([model if model else cls.default_model for model in all_models]))
return cls.models
@classmethod @classmethod
async def create_async_generator( async def create_async_generator(
@ -116,6 +118,7 @@ class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin):
conversation: JsonConversation = None, conversation: JsonConversation = None,
**kwargs **kwargs
) -> AsyncResult: ) -> AsyncResult:
cls.get_models(ignored=ignored)
providers = [] providers = []
if model and ":" in model: if model and ":" in model:
providers = model.split(":") providers = model.split(":")
@ -146,8 +149,8 @@ class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin):
providers.append(provider) providers.append(provider)
else: else:
for provider in [ for provider in [
OpenaiChat, Cloudflare, LMArenaProvider, PerplexityLabs, Gemini, Grok, DeepSeekAPI, FreeRouter, Blackbox, OpenaiChat, Cloudflare, HarProvider, PerplexityLabs, Gemini, Grok, DeepSeekAPI, FreeRouter, Blackbox,
HuggingFace, HuggingFaceMedia, HuggingSpace, LambdaChat, CopilotAccount, PollinationsAI, DeepInfraChat HuggingSpace, LambdaChat, CopilotAccount, PollinationsAI, DeepInfraChat
]: ]:
if provider.working: if provider.working:
if not model or model in provider.get_models() or model in provider.model_aliases: if not model or model in provider.get_models() or model in provider.model_aliases:

View file

@ -583,7 +583,7 @@ async def get_async_streaming(bucket_dir: str, delete_files = False, refine_chun
raise e raise e
def get_tempfile(file, suffix): def get_tempfile(file, suffix):
copyfile = tempfile.NamedTemporaryFile(suffix=suffix, delete=False) copyfile = tempfile.NamedTemporaryFile(suffix=os.path.splitext(suffix)[-1], delete=False)
shutil.copyfileobj(file, copyfile) shutil.copyfileobj(file, copyfile)
copyfile.close() copyfile.close()
file.close() file.close()