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
import os
import asyncio
from typing import Any
try:
from markitdown import MarkItDown as MaItDo, StreamInfo
@ -21,6 +23,7 @@ class MarkItDown(AsyncGeneratorProvider, ProviderModelMixin):
model: str,
messages: Messages,
media: MediaListType = None,
llm_client: Any = None,
**kwargs
) -> AsyncResult:
if media is None:
@ -31,11 +34,28 @@ class MarkItDown(AsyncGeneratorProvider, ProviderModelMixin):
for file, filename in media:
text = None
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:
copyfile = get_tempfile(file, filename)
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:
os.remove(copyfile)
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_5_Max import Qwen_Qwen_2_5_Max
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 .Voodoohop_Flux1Schnell import Voodoohop_Flux1Schnell
@ -38,6 +39,7 @@ class HuggingSpace(AsyncGeneratorProvider, ProviderModelMixin):
Qwen_Qwen_2_5M,
Qwen_Qwen_2_5_Max,
Qwen_Qwen_2_72B,
Qwen_Qwen_3,
StabilityAI_SD35Large,
Voodoohop_Flux1Schnell,
]

View file

@ -337,7 +337,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
timeout=timeout
) as session:
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:
cls._create_request_args(cls._cookies)
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)
await raise_for_status(response)
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:
debug.error("OpenaiChat: Upload image failed")
debug.error(e)

View file

@ -508,13 +508,19 @@ class Api:
provider: Annotated[Optional[str], Form()] = None,
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:
response = await self.client.chat.completions.create(
messages=prompt,
model=model,
provider=provider if path_provider is None else path_provider,
provider=provider,
media=[[file.file, file.filename]],
modalities=["text"]
**kwargs
)
return {"text": response.choices[0].message.content, "model": response.model, "provider": response.provider}
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 ..providers.response import JsonConversation, ProviderInfo
from ..Provider.needs_auth import OpenaiChat, CopilotAccount
from ..Provider.hf import HuggingFace, HuggingFaceMedia
from ..Provider.hf_space import HuggingSpace
from .. import Provider
from .. import models
from ..Provider import Cloudflare, LMArenaProvider, Gemini, Grok, DeepSeekAPI, PerplexityLabs, LambdaChat, PollinationsAI, FreeRouter
from ..Provider import Microsoft_Phi_4, DeepInfraChat, Blackbox, EdgeTTS, gTTS, MarkItDown
from ..Provider import Cloudflare, Gemini, Grok, DeepSeekAPI, PerplexityLabs, LambdaChat, PollinationsAI, FreeRouter
from ..Provider import Microsoft_Phi_4, DeepInfraChat, Blackbox, EdgeTTS, gTTS, MarkItDown, HarProvider
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin):
@ -20,6 +19,7 @@ class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin):
@classmethod
def get_models(cls, ignored: list[str] = []) -> list[str]:
if not cls.models:
cls.audio_models = {}
cls.image_models = []
cls.vision_models = []
@ -80,7 +80,6 @@ class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin):
).replace("-03-26", ""
).replace("-01-21", ""
).replace("-002", ""
).replace(".1-", "-"
).replace("_", "."
).replace("c4ai-", ""
).replace("-preview", ""
@ -90,10 +89,12 @@ class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin):
).replace("-bf16", ""
).replace("-hf", ""
).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:
continue
model_map = {clean_name(model): model for model in provider.get_models()}
if not provider.model_aliases:
provider.model_aliases = {}
provider.model_aliases.update(model_map)
all_models.extend(list(model_map.keys()))
cls.image_models.extend([clean_name(model) for model in provider.image_models])
@ -103,7 +104,8 @@ class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin):
if provider.working and getattr(provider, "parent", provider.__name__) not in ignored:
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)})
return list(dict.fromkeys([model if model else cls.default_model for model in all_models]))
cls.models = list(dict.fromkeys([model if model else cls.default_model for model in all_models]))
return cls.models
@classmethod
async def create_async_generator(
@ -116,6 +118,7 @@ class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin):
conversation: JsonConversation = None,
**kwargs
) -> AsyncResult:
cls.get_models(ignored=ignored)
providers = []
if model and ":" in model:
providers = model.split(":")
@ -146,8 +149,8 @@ class AnyProvider(AsyncGeneratorProvider, ProviderModelMixin):
providers.append(provider)
else:
for provider in [
OpenaiChat, Cloudflare, LMArenaProvider, PerplexityLabs, Gemini, Grok, DeepSeekAPI, FreeRouter, Blackbox,
HuggingFace, HuggingFaceMedia, HuggingSpace, LambdaChat, CopilotAccount, PollinationsAI, DeepInfraChat
OpenaiChat, Cloudflare, HarProvider, PerplexityLabs, Gemini, Grok, DeepSeekAPI, FreeRouter, Blackbox,
HuggingSpace, LambdaChat, CopilotAccount, PollinationsAI, DeepInfraChat
]:
if provider.working:
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
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)
copyfile.close()
file.close()