From ddadc36fa845cce392a95bf917f25f6b76b9dad0 Mon Sep 17 00:00:00 2001 From: hlohaus <983577+hlohaus@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:20:09 +0200 Subject: [PATCH] Enhance Perplexity provider with additional models and improved conversation handling; add JsonRequest and JsonResponse classes for better response management --- g4f/Provider/Perplexity.py | 152 ++++++++++++++++++------ g4f/Provider/PollinationsAI.py | 3 +- g4f/Provider/template/OpenaiTemplate.py | 5 +- g4f/client/__init__.py | 126 ++++---------------- g4f/gui/server/api.py | 4 + g4f/providers/response.py | 27 +++++ 6 files changed, 179 insertions(+), 138 deletions(-) diff --git a/g4f/Provider/Perplexity.py b/g4f/Provider/Perplexity.py index 9042f3e7..b9c0e620 100644 --- a/g4f/Provider/Perplexity.py +++ b/g4f/Provider/Perplexity.py @@ -6,7 +6,7 @@ import uuid from ..typing import AsyncResult, Messages, Cookies from ..requests import StreamSession, raise_for_status, sse_stream from ..cookies import get_cookies -from ..providers.response import ProviderInfo +from ..providers.response import ProviderInfo, JsonConversation, JsonRequest, JsonResponse, Reasoning from .base_provider import AsyncGeneratorProvider, ProviderModelMixin from .. import debug @@ -21,21 +21,63 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin): models = [ default_model, "turbo", + "gpt41", + "gpt5", + "gpt5_thinking", + "o3", + "o3pro", + "claude2", + "claude37sonnetthinking", + "claude40opus", + "claude40opusthinking", + "claude41opusthinking", + "claude45sonnet", + "claude45sonnetthinking", + "experimental", + "grok", + "grok4", + "gemini2flash", "pplx_pro", - "gpt-5", + "pplx_pro_upgraded", + "pplx_alpha", + "pplx_beta", + "comet_max_assistant", + "o3_research", + "o3pro_research", + "claude40sonnet_research", + "claude40sonnetthinking_research", + "claude40opus_research", + "claude40opusthinking_research", + "o3_labs", + "o3pro_labs", + "claude40sonnetthinking_labs", + "claude40opusthinking_labs", + "o4mini", + "o1", + "gpt4o", + "gpt45", + "gpt4", + "o3mini", + "claude35haiku", + "llama_x_large", + "mistral", + "claude3opus", + "gemini", + "pplx_reasoning", + "r1" ] model_aliases = { "gpt-5": "gpt5", + "gpt-5-thinking": "gpt5_thinking", } - _user_id = None - @classmethod async def create_async_generator( cls, model: str, messages: Messages, cookies: Cookies = None, + conversation: JsonConversation = None, proxy: str = None, **kwargs ) -> AsyncResult: @@ -43,13 +85,13 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin): model = cls.default_model if cookies is None: cookies = get_cookies(cls.cookie_domain, False) - else: - cls._user_id = None - - # Generate UUIDs for request tracking - frontend_uuid = str(uuid.uuid4()) - frontend_context_uuid = str(uuid.uuid4()) - visitor_id = str(uuid.uuid4()) + if conversation is None: + conversation = JsonConversation( + frontend_uid=str(uuid.uuid4()), + frontend_context_uuid=str(uuid.uuid4()), + visitor_id=str(uuid.uuid4()), + user_id=None, + ) request_id = str(uuid.uuid4()) headers = { @@ -63,7 +105,7 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin): "x-perplexity-request-reason": "perplexity-query-state-provider", "x-request-id": request_id, } - + # Extract the last user message as the query query = "" for message in reversed(messages): @@ -72,18 +114,19 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin): break async with StreamSession(headers=headers, cookies=cookies, proxy=proxy, impersonate="chrome") as session: - if cls._user_id is None: + if conversation.user_id is None: async with session.get(f"{cls.url}/api/auth/session") as response: await raise_for_status(response) user = await response.json() - cls._user_id = user.get("user", {}).get("id") - debug.log(f"Perplexity user id: {cls._user_id}") + conversation.user_id = user.get("user", {}).get("id") + debug.log(f"Perplexity user id: {conversation.user_id}") + yield conversation if model == "auto": - model = "pplx_pro" if cls._user_id else "turbo" + model = "pplx_pro" if conversation.user_id else "turbo" yield ProviderInfo(**cls.get_dict(), model=model) if model in cls.model_aliases: model = cls.model_aliases[model] - if cls._user_id is None: + if conversation.user_id is None: data = { "params": { "attachments": [], @@ -92,13 +135,13 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin): "search_focus": "internet", "sources": ["web"], "search_recency_filter": None, - "frontend_uuid": frontend_uuid, + "frontend_uuid": conversation.frontend_uid, "mode": "concise", "model_preference": model, "is_related_query": False, "is_sponsored": False, - "visitor_id": visitor_id, - "frontend_context_uuid": frontend_context_uuid, + "visitor_id": conversation.visitor_id, + "frontend_context_uuid": conversation.frontend_context_uuid, "prompt_source": "user", "query_source": "home", "is_incognito": False, @@ -144,53 +187,92 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin): data = { "params": { "last_backend_uuid": None, - "read_write_token": "457a2d3d-c53f-4065-8554-7645a36fc220", + "read_write_token": None, "attachments": [], "language": "en-US", "timezone": "America/New_York", "search_focus": "internet", - "sources": ["web"], - "frontend_uuid": frontend_uuid, + "sources": [ + "web" + ], + "frontend_uuid": conversation.frontend_uid, "mode": "copilot", - "model_preference": "gpt5", + "model_preference": model, "is_related_query": False, "is_sponsored": False, - "visitor_id": visitor_id, - "user_nextauth_id": cls._user_id, + "visitor_id": conversation.visitor_id, + "user_nextauth_id": conversation.user_id, "prompt_source": "user", - "query_source":"followup", + "query_source": "followup", "is_incognito": False, "time_from_first_type": random.randint(0, 1000), "local_search_enabled": False, "use_schematized_api": True, "send_back_text_in_streaming_api": False, - "supported_block_use_cases": ["answer_modes", "media_items", "knowledge_cards", "inline_entity_cards", "place_widgets", "finance_widgets", "sports_widgets", "shopping_widgets", "jobs_widgets", "search_result_widgets", "clarification_responses", "inline_images", "inline_assets", "inline_finance_widgets", "placeholder_cards", "diff_blocks", "inline_knowledge_cards", "entity_group_v2", "refinement_filters", "canvas_mode"], + "supported_block_use_cases": [ + "answer_modes", + "media_items", + "knowledge_cards", + "inline_entity_cards", + "place_widgets", + "finance_widgets", + "sports_widgets", + "shopping_widgets", + "jobs_widgets", + "search_result_widgets", + "clarification_responses", + "inline_images", + "inline_assets", + "inline_finance_widgets", + "placeholder_cards", + "diff_blocks", + "inline_knowledge_cards", + "entity_group_v2", + "refinement_filters", + "canvas_mode" + ], "client_coordinates": None, "mentions": [], "skip_search_enabled": True, "is_nav_suggestions_disabled": False, "followup_source": "link", + "always_search_override": False, + "override_no_search": False, + "comet_max_assistant_enabled": False, "version": "2.18" }, "query_str": query } + yield JsonRequest.from_dict(data) async with session.post( f"{cls.url}/rest/sse/perplexity_ask", json=data, ) as response: await raise_for_status(response) full_response = "" - last_response = "" + full_reasoning = "" async for json_data in sse_stream(response): + yield JsonResponse.from_dict(json_data) for block in json_data.get("blocks", []): for patch in block.get("diff_block", {}).get("patches", []): + if patch.get("path") == "/progress": + continue value = patch.get("value", "") + if patch.get("path").startswith("/goals"): + if isinstance(value, str): + if value.startswith(full_reasoning): + value = value[len(full_reasoning):] + yield Reasoning(value) + full_reasoning += value + else: + yield Reasoning(status="") + continue + if block.get("diff_block").get("field") != "markdown_block": + continue value = value.get("answer", "") if isinstance(value, dict) else value - if value: + if value and isinstance(value, str): if value.startswith(full_response): value = value[len(full_response):] - if value.startswith(last_response): - value = value[len(last_response):] - last_response = value - full_response += value - yield value + if value: + full_response += value + yield value diff --git a/g4f/Provider/PollinationsAI.py b/g4f/Provider/PollinationsAI.py index 2c022653..ceb1e46e 100644 --- a/g4f/Provider/PollinationsAI.py +++ b/g4f/Provider/PollinationsAI.py @@ -17,7 +17,7 @@ from ..errors import MissingAuthError from ..requests.raise_for_status import raise_for_status from ..requests.aiohttp import get_connector from ..image import use_aspect_ratio -from ..providers.response import ImageResponse, Reasoning, TitleGeneration, SuggestedFollowups +from ..providers.response import ImageResponse, Reasoning, TitleGeneration, SuggestedFollowups, JsonRequest from ..tools.media import render_messages from ..config import STATIC_URL from .template.OpenaiTemplate import read_response @@ -461,6 +461,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin): headers = {"referer": referrer} if api_key: headers["authorization"] = f"Bearer {api_key}" + yield JsonRequest.from_dict(data) async with session.post(cls.openai_endpoint, json=data, headers=headers) as response: if response.status in (400, 500): debug.error(f"Error: {response.status} - Bad Request: {data}") diff --git a/g4f/Provider/template/OpenaiTemplate.py b/g4f/Provider/template/OpenaiTemplate.py index aedbb3f6..5813690f 100644 --- a/g4f/Provider/template/OpenaiTemplate.py +++ b/g4f/Provider/template/OpenaiTemplate.py @@ -8,7 +8,7 @@ from ...typing import Union, AsyncResult, Messages, MediaListType from ...requests import StreamSession, StreamResponse, raise_for_status, sse_stream from ...image import use_aspect_ratio from ...image.copy_images import save_response_media -from ...providers.response import FinishReason, ToolCalls, Usage, ImageResponse, ProviderInfo, AudioResponse, Reasoning, JsonConversation +from ...providers.response import * from ...tools.media import render_messages from ...tools.run_tools import AuthManager from ...errors import MissingAuthError @@ -150,6 +150,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin api_endpoint = f"{api_base.rstrip('/')}/chat/completions" if api_endpoint is None: api_endpoint = cls.api_endpoint + yield JsonRequest.from_dict(data) async with session.post(api_endpoint, json=data, ssl=cls.ssl) as response: async for chunk in read_response(response, stream, prompt, cls.get_dict(), download_media): yield chunk @@ -170,6 +171,7 @@ async def read_response(response: StreamResponse, stream: bool, prompt: str, pro content_type = response.headers.get("content-type", "text/event-stream" if stream else "application/json") if content_type.startswith("application/json"): data = await response.json() + yield JsonResponse.from_dict(data) OpenaiTemplate.raise_error(data, response.status) await raise_for_status(response) model = data.get("model") @@ -206,6 +208,7 @@ async def read_response(response: StreamResponse, stream: bool, prompt: str, pro first = True model_returned = False async for data in sse_stream(response): + yield JsonResponse.from_dict(data) OpenaiTemplate.raise_error(data) model = data.get("model") if not model_returned and model: diff --git a/g4f/client/__init__.py b/g4f/client/__init__.py index 4878d58d..eb77a975 100644 --- a/g4f/client/__init__.py +++ b/g4f/client/__init__.py @@ -261,53 +261,6 @@ async def async_iter_response( finally: await safe_aclose(response) -async def async_response( - response: AsyncIterator[Union[str, ResponseType]] -) -> ClientResponse: - content = "" - response_id = ''.join(random.choices(string.ascii_letters + string.digits, k=28)) - idx = 0 - usage = None - provider: ProviderInfo = None - conversation: JsonConversation = None - - async for chunk in response: - if isinstance(chunk, FinishReason): - continue - elif isinstance(chunk, JsonConversation): - conversation = chunk - continue - elif isinstance(chunk, ToolCalls): - continue - elif isinstance(chunk, Usage): - usage = chunk - continue - elif isinstance(chunk, ProviderInfo): - provider = chunk - continue - elif isinstance(chunk, HiddenResponse): - continue - elif isinstance(chunk, Exception): - continue - - content = add_chunk(content, chunk) - if not content: - continue - idx += 1 - - if usage is None: - usage = UsageModel.model_construct(completion_tokens=idx, total_tokens=idx) - else: - usage = UsageModel.model_construct(**usage.get_dict()) - - response = ClientResponse.model_construct( - content, response_id, int(time.time()), usage=usage, conversation=conversation - ) - if provider is not None: - response.provider = provider.name - response.model = provider.model - return response - async def async_iter_append_model_and_provider( response: AsyncChatCompletionResponseType, last_model: str, @@ -361,6 +314,7 @@ class Completions: stop: Optional[Union[list[str], str]] = None, api_key: Optional[str] = None, ignore_stream: Optional[bool] = False, + raw: Optional[bool] = False, **kwargs ) -> ChatCompletion: if isinstance(messages, str): @@ -392,12 +346,21 @@ class Completions: **kwargs ) + if raw: + def filter_raw(response): + for chunk in response: + if isinstance(chunk, JsonResponse): + yield chunk + raw_response = filter_raw(response) + if stream: + return raw_response + return next(raw_response) + response = iter_response(response, stream, response_format, max_tokens, stop) response = iter_append_model_and_provider(response, model, provider) if stream: return response - else: - return next(response) + return next(response) def stream( self, @@ -655,7 +618,6 @@ class AsyncClient(BaseClient): self.models: ClientModels = ClientModels(self, provider, media_provider) self.images: AsyncImages = AsyncImages(self, media_provider) self.media: AsyncImages = self.images - self.responses: AsyncResponses = AsyncResponses(self, provider) class AsyncChat: completions: AsyncCompletions @@ -682,6 +644,7 @@ class AsyncCompletions: stop: Optional[Union[list[str], str]] = None, api_key: Optional[str] = None, ignore_stream: Optional[bool] = False, + raw: Optional[bool] = False, **kwargs ) -> Awaitable[ChatCompletion]: if isinstance(messages, str): @@ -713,13 +676,22 @@ class AsyncCompletions: **kwargs ) + if raw: + async def filter_raw(response): + async for chunk in response: + if isinstance(chunk, JsonResponse): + yield chunk + raw_response = filter_raw(response) + if stream: + return raw_response + return next(raw_response) + response = async_iter_response(response, stream, response_format, max_tokens, stop) response = async_iter_append_model_and_provider(response, model, provider) if stream: return response - else: - return anext(response) + return anext(response) def stream( self, @@ -754,52 +726,4 @@ class AsyncImages(Images): ) -> ImagesResponse: return await self.async_create_variation( image=image, model=model, provider=provider, response_format=response_format, **kwargs - ) - -class AsyncResponses(): - def __init__(self, client: AsyncClient, provider: Optional[ProviderType] = None): - self.client: AsyncClient = client - self.provider: ProviderType = provider - - async def create( - self, - input: str, - model: str = "", - provider: Optional[ProviderType] = None, - instructions: Optional[str] = None, - proxy: Optional[str] = None, - api_key: Optional[str] = None, - **kwargs - ) -> ClientResponse: - if isinstance(input, str): - input = [{"role": "user", "content": input}] - if instructions is not None: - input = [{"role": "developer", "content": instructions}] + input - for idx, message in enumerate(input): - if isinstance(message["content"], list): - for key, value in enumerate(message["content"]): - if isinstance(value, dict) and value.get("type") == "input_text": - message["content"][key] = {"type": "text", "text": value.get("text")} - input[idx] = {"role": message["role"], "content": message["content"]} - resolve_media(kwargs) - if hasattr(model, "name"): - model = model.get_long_name() - if provider is None: - provider = self.provider - if provider is None: - provider = AnyProvider - if isinstance(provider, str): - provider = convert_to_provider(provider) - - response = async_iter_run_tools( - provider, - model=model, - messages=input, - **filter_none( - proxy=self.client.proxy if proxy is None else proxy, - api_key=self.client.api_key if api_key is None else api_key - ), - **kwargs - ) - - return await async_response(response) + ) \ No newline at end of file diff --git a/g4f/gui/server/api.py b/g4f/gui/server/api.py index cd767c9e..23fc780e 100644 --- a/g4f/gui/server/api.py +++ b/g4f/gui/server/api.py @@ -274,6 +274,10 @@ class Api: yield self._format_json("continue", chunk.log) elif isinstance(chunk, RawResponse): yield self._format_json(chunk.type, **chunk.get_dict()) + elif isinstance(chunk, JsonRequest): + yield self._format_json("request", chunk.get_dict()) + elif isinstance(chunk, JsonResponse): + yield self._format_json("response", chunk.get_dict()) else: yield self._format_json("content", str(chunk)) except MissingAuthError as e: diff --git a/g4f/providers/response.py b/g4f/providers/response.py index 9c889f02..de38ae5c 100644 --- a/g4f/providers/response.py +++ b/g4f/providers/response.py @@ -151,6 +151,33 @@ class JsonMixin: class RawResponse(ResponseType, JsonMixin): pass +class ObjectMixin: + def __init__(self, **kwargs) -> None: + """Initialize with keyword arguments as attributes.""" + for key, value in kwargs.items(): + setattr(self, key, ObjectMixin.from_dict(value) if isinstance(value, dict) else [ObjectMixin.from_dict(v) if isinstance(v, dict) else v for v in value] if isinstance(value, list) else value) + + def get_dict(self) -> Dict: + """Return a dictionary of non-private attributes.""" + return { + key: value.get_dict() if isinstance(value, ObjectMixin) else [v.get_dict() if isinstance(v, ObjectMixin) else v for v in value] if isinstance(value, list) else value + for key, value in self.__dict__.items() + if not key.startswith("__") + } + + @classmethod + def from_dict(cls, data: Dict) -> JsonMixin: + """Create an instance from a dictionary.""" + return cls(**data) + +class JsonResponse(ResponseType, ObjectMixin): + def __str__(self) -> str: + return str(self.get_dict()) + +class JsonRequest(ResponseType, ObjectMixin): + def __str__(self) -> str: + return str(self.get_dict()) + class HiddenResponse(ResponseType): def __str__(self) -> str: """Hidden responses return an empty string."""