Support "Think Deeper" in Copilot

Improve Documentation
This commit is contained in:
hlohaus
2025-03-30 15:40:46 +02:00
parent c471c53671
commit 8c2c46b20f
14 changed files with 189 additions and 82 deletions

View File

@@ -248,6 +248,7 @@ Run the Web UI on your smartphone for easy access on the go. Check out the dedic
- **File API from G4F:** [/docs/file](docs/file.md) - **File API from G4F:** [/docs/file](docs/file.md)
- **PydanticAI and LangChain Integration for G4F:** [/docs/pydantic_ai](docs/pydantic_ai.md) - **PydanticAI and LangChain Integration for G4F:** [/docs/pydantic_ai](docs/pydantic_ai.md)
- **Legacy API with python modules:** [/docs/legacy](docs/legacy.md) - **Legacy API with python modules:** [/docs/legacy](docs/legacy.md)
- **G4F - Media Documentation** [/docs/media](/docs/meda.md) *(New)*
--- ---

View File

@@ -126,7 +126,7 @@ providers = {
} }
for provider_name, api_key in providers.items(): for provider_name, api_key in providers.items():
client = Client(provider=f"g4f.Provider.{provider_name}", api_key=api_key) client = Client(provider=provider_name, api_key=api_key)
response = client.chat.completions.create( response = client.chat.completions.create(
model="claude-3.5-sonnet", model="claude-3.5-sonnet",
messages=[{"role": "user", "content": f"Hello from {provider_name}!"}] messages=[{"role": "user", "content": f"Hello from {provider_name}!"}]
@@ -144,16 +144,22 @@ for provider_name, api_key in providers.items():
- Firefox: **Storage** → **Cookies** - Firefox: **Storage** → **Cookies**
```python ```python
from g4f.client import Client
from g4f.Provider import Gemini from g4f.Provider import Gemini
# Initialize with cookies # Using with cookies
client = Client( client = Client(
provider=Gemini, provider=Gemini,
)
response = client.chat.completions.create(
model="", # Default model
messages="Hello Google",
cookies={ cookies={
"__Secure-1PSID": "your_cookie_value_here", "__Secure-1PSID": "your_cookie_value_here",
"__Secure-1PSIDTS": "timestamp_value_here" "__Secure-1PSIDTS": "timestamp_value_here"
} }
) )
print(f"Gemini: {response.choices[0].message.content}")
``` ```
--- ---

View File

@@ -69,9 +69,9 @@ The G4F GUI is a self-contained, user-friendly interface designed for interactin
- **Basic Authentication** - **Basic Authentication**
You can set a password for Basic Authentication using the `--g4f-api-key` argument when starting the web server. You can set a password for Basic Authentication using the `--g4f-api-key` argument when starting the web server.
### 9. **Continue Button (ChatGPT & HuggingChat)** ### 9. **Continue Button**
- **Automatic Detection of Truncated Responses** - **Automatic Detection of Truncated Responses**
When using **ChatGPT** or **HuggingChat** providers, responses may occasionally be cut off or truncated. When using providers, responses may occasionally be cut off or truncated.
- **Continue Button** - **Continue Button**
If the GUI detects that the response ended abruptly, a **Continue** button appears directly below the truncated message. Clicking this button sends a follow-up request to the same provider and model, retrieving the rest of the message. If the GUI detects that the response ended abruptly, a **Continue** button appears directly below the truncated message. Clicking this button sends a follow-up request to the same provider and model, retrieving the rest of the message.
- **Seamless Conversation Flow** - **Seamless Conversation Flow**
@@ -154,7 +154,7 @@ http://localhost:8080/chat/
- **Text/Code:** The generated response appears in the conversation window. - **Text/Code:** The generated response appears in the conversation window.
- **Images:** Generated images are displayed as thumbnails. Click on any thumbnail to view it in full size within the lightbox. - **Images:** Generated images are displayed as thumbnails. Click on any thumbnail to view it in full size within the lightbox.
5. **Continue Button (ChatGPT & HuggingChat)** 5. **Continue Button**
- If a response is truncated, a **Continue** button will appear under the last message. Clicking it asks the same provider to continue the response from where it ended. - If a response is truncated, a **Continue** button will appear under the last message. Clicking it asks the same provider to continue the response from where it ended.
6. **Manage Conversations** 6. **Manage Conversations**

View File

@@ -175,7 +175,7 @@ from g4f.Provider import HuggingFaceMedia
async def main(): async def main():
client = AsyncClient( client = AsyncClient(
provider=HuggingFaceMedia, provider=HuggingFaceMedia,
api_key="hf_***" # Your API key here api_key=os.getenv("HF_TOKEN") # Your API key here
) )
video_models = client.models.get_video() video_models = client.models.get_video()
@@ -214,7 +214,7 @@ from g4f.Provider import HuggingFaceMedia
async def main(): async def main():
client = AsyncClient( client = AsyncClient(
provider=HuggingFaceMedia, provider=HuggingFaceMedia,
api_key=os.getenv("HUGGINGFACE_API_KEY") # Your API key here api_key=os.getenv("HF_TOKEN") # Your API key here
) )
video_models = client.models.get_video() video_models = client.models.get_video()

View File

@@ -83,9 +83,9 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
pass pass
data = { data = {
"messages": [{ "messages": [{
"role":"user", **message,
"content": message["content"] if isinstance(message["content"], str) else "", "content": message["content"] if isinstance(message["content"], str) else "",
"parts": [{"type":"text", "text":message["content"]}] if isinstance(message["content"], str) else message["content"]} for message in messages], "parts": [{"type":"text", "text":message["content"]}] if isinstance(message["content"], str) else message} for message in messages],
"lora": None, "lora": None,
"model": model, "model": model,
"max_tokens": max_tokens, "max_tokens": max_tokens,

View File

@@ -24,7 +24,7 @@ from .openai.har_file import get_headers, get_har_files
from ..typing import CreateResult, Messages, MediaListType from ..typing import CreateResult, Messages, MediaListType
from ..errors import MissingRequirementsError, NoValidHarFileError, MissingAuthError from ..errors import MissingRequirementsError, NoValidHarFileError, MissingAuthError
from ..requests.raise_for_status import raise_for_status from ..requests.raise_for_status import raise_for_status
from ..providers.response import BaseConversation, JsonConversation, RequestLogin, ImageResponse from ..providers.response import BaseConversation, JsonConversation, RequestLogin, ImageResponse, FinishReason, SuggestedFollowups
from ..providers.asyncio import get_running_loop from ..providers.asyncio import get_running_loop
from ..tools.media import merge_media from ..tools.media import merge_media
from ..requests import get_nodriver from ..requests import get_nodriver
@@ -46,10 +46,13 @@ class Copilot(AbstractProvider, ProviderModelMixin):
supports_stream = True supports_stream = True
default_model = "Copilot" default_model = "Copilot"
models = [default_model] models = [default_model, "Think Deeper"]
model_aliases = { model_aliases = {
"gpt-4": default_model, "gpt-4": default_model,
"o1": default_model, "gpt-4o": default_model,
"o1": "Think Deeper",
"reasoning": "Think Deeper",
"dall-e-3": default_model
} }
websocket_url = "wss://copilot.microsoft.com/c/api/chat?api-version=2" websocket_url = "wss://copilot.microsoft.com/c/api/chat?api-version=2"
@@ -75,10 +78,10 @@ class Copilot(AbstractProvider, ProviderModelMixin):
) -> CreateResult: ) -> CreateResult:
if not has_curl_cffi: if not has_curl_cffi:
raise MissingRequirementsError('Install or update "curl_cffi" package | pip install -U curl_cffi') raise MissingRequirementsError('Install or update "curl_cffi" package | pip install -U curl_cffi')
model = cls.get_model(model)
websocket_url = cls.websocket_url websocket_url = cls.websocket_url
headers = None headers = None
if cls.needs_auth or media is not None: if cls._access_token:
if api_key is not None: if api_key is not None:
cls._access_token = api_key cls._access_token = api_key
if cls._access_token is None: if cls._access_token is None:
@@ -163,6 +166,7 @@ class Copilot(AbstractProvider, ProviderModelMixin):
# "token": clarity_token, # "token": clarity_token,
# "method":"clarity" # "method":"clarity"
# }).encode(), CurlWsFlag.TEXT) # }).encode(), CurlWsFlag.TEXT)
wss.send(json.dumps({"event":"setOptions","supportedCards":["weather","local","image","sports","video","ads","finance"],"ads":{"supportedTypes":["multimedia","product","tourActivity","propertyPromotion","text"]}}));
wss.send(json.dumps({ wss.send(json.dumps({
"event": "send", "event": "send",
"conversationId": conversation_id, "conversationId": conversation_id,
@@ -170,7 +174,7 @@ class Copilot(AbstractProvider, ProviderModelMixin):
"type": "text", "type": "text",
"text": prompt, "text": prompt,
}], }],
"mode": "chat" "mode": "reasoning" if "Think" in model else "chat",
}).encode(), CurlWsFlag.TEXT) }).encode(), CurlWsFlag.TEXT)
is_started = False is_started = False
@@ -193,6 +197,10 @@ class Copilot(AbstractProvider, ProviderModelMixin):
elif msg.get("event") == "imageGenerated": elif msg.get("event") == "imageGenerated":
yield ImageResponse(msg.get("url"), image_prompt, {"preview": msg.get("thumbnailUrl")}) yield ImageResponse(msg.get("url"), image_prompt, {"preview": msg.get("thumbnailUrl")})
elif msg.get("event") == "done": elif msg.get("event") == "done":
yield FinishReason("stop")
break
elif msg.get("event") == "suggestedFollowups":
yield SuggestedFollowups(msg.get("suggestions"))
break break
elif msg.get("event") == "replaceText": elif msg.get("event") == "replaceText":
yield msg.get("text") yield msg.get("text")

View File

@@ -0,0 +1,85 @@
from __future__ import annotations
import asyncio
try:
from duckduckgo_search import DDGS
from duckduckgo_search.exceptions import DuckDuckGoSearchException, RatelimitException, ConversationLimitException
has_requirements = True
except ImportError:
has_requirements = False
try:
import nodriver
has_nodriver = True
except ImportError:
has_nodriver = False
from ..typing import AsyncResult, Messages
from ..requests import get_nodriver
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
from .helper import get_last_user_message
class DuckDuckGo(AsyncGeneratorProvider, ProviderModelMixin):
label = "Duck.ai (duckduckgo_search)"
url = "https://duckduckgo.com/aichat"
api_base = "https://duckduckgo.com/duckchat/v1/"
working = False
supports_stream = True
supports_system_message = True
supports_message_history = True
default_model = "gpt-4o-mini"
models = [default_model, "meta-llama/Llama-3.3-70B-Instruct-Turbo", "claude-3-haiku-20240307", "o3-mini", "mistralai/Mistral-Small-24B-Instruct-2501"]
ddgs: DDGS = None
model_aliases = {
"gpt-4": "gpt-4o-mini",
"llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
"claude-3-haiku": "claude-3-haiku-20240307",
"mixtral-small-24b": "mistralai/Mistral-Small-24B-Instruct-2501",
}
@classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
timeout: int = 60,
**kwargs
) -> AsyncResult:
if not has_requirements:
raise ImportError("duckduckgo_search is not installed. Install it with `pip install duckduckgo-search`.")
if cls.ddgs is None:
cls.ddgs = DDGS(proxy=proxy, timeout=timeout)
if has_nodriver:
await cls.nodriver_auth(proxy=proxy)
model = cls.get_model(model)
for chunk in cls.ddgs.chat_yield(get_last_user_message(messages), model, timeout):
yield chunk
@classmethod
async def nodriver_auth(cls, proxy: str = None):
browser, stop_browser = await get_nodriver(proxy=proxy)
try:
page = browser.main_tab
def on_request(event: nodriver.cdp.network.RequestWillBeSent, page=None):
if cls.api_base in event.request.url:
if "X-Vqd-4" in event.request.headers:
cls.ddgs._chat_vqd = event.request.headers["X-Vqd-4"]
if "X-Vqd-Hash-1" in event.request.headers:
cls.ddgs._chat_vqd_hash = event.request.headers["X-Vqd-Hash-1"]
if "F-Fe-Version" in event.request.headers:
cls.ddgs._chat_xfe = event.request.headers["F-Fe-Version" ]
await page.send(nodriver.cdp.network.enable())
page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
page = await browser.get(cls.url)
while True:
if cls.ddgs._chat_vqd:
break
await asyncio.sleep(1)
await page.close()
finally:
stop_browser()

View File

@@ -40,6 +40,7 @@ try:
from .Copilot import Copilot from .Copilot import Copilot
from .DDG import DDG from .DDG import DDG
from .DeepInfraChat import DeepInfraChat from .DeepInfraChat import DeepInfraChat
from .DuckDuckGo import DuckDuckGo
from .Dynaspark import Dynaspark from .Dynaspark import Dynaspark
except ImportError as e: except ImportError as e:
debug.error("Providers not loaded (A-D):", e) debug.error("Providers not loaded (A-D):", e)

View File

@@ -19,11 +19,6 @@ class CopilotAccount(AsyncAuthedProvider, Copilot):
parent = "Copilot" parent = "Copilot"
default_model = "Copilot" default_model = "Copilot"
default_vision_model = default_model default_vision_model = default_model
models = [default_model]
image_models = models
model_aliases = {
"dall-e-3": default_model
}
@classmethod @classmethod
async def on_auth_async(cls, proxy: str = None, **kwargs) -> AsyncIterator: async def on_auth_async(cls, proxy: str = None, **kwargs) -> AsyncIterator:

View File

@@ -68,6 +68,7 @@ class ImageGenerationConfig(BaseModel):
aspect_ratio: Optional[str] = None aspect_ratio: Optional[str] = None
n: Optional[int] = None n: Optional[int] = None
negative_prompt: Optional[str] = None negative_prompt: Optional[str] = None
resolution: Optional[str] = None
class ProviderResponseModel(BaseModel): class ProviderResponseModel(BaseModel):
id: str id: str

View File

@@ -1325,7 +1325,12 @@ const delete_conversation = async (conversation_id) => {
} }
} }
} }
if (window.share_id && conversation_id == window.start_id) {
const url = `${window.share_url}/backend-api/v2/files/${window.share_id}`;
await fetch(url, {
method: 'DELETE'
});
}
appStorage.removeItem(`conversation:${conversation_id}`); appStorage.removeItem(`conversation:${conversation_id}`);
const item = document.getElementById(`convo-${conversation_id}`); const item = document.getElementById(`convo-${conversation_id}`);
item.remove(); item.remove();
@@ -2622,7 +2627,7 @@ function connectToSSE(url, do_refine, bucket_id) {
} else if (data.action == "media") { } else if (data.action == "media") {
inputCount.innerText = `File: ${data.filename}`; inputCount.innerText = `File: ${data.filename}`;
const url = `/files/${bucket_id}/media/${data.filename}`; const url = `/files/${bucket_id}/media/${data.filename}`;
const media = [{bucket_id: bucket_id, url: url}]; const media = [{bucket_id: bucket_id, url: url, name: data.filename}];
await handle_ask(false, media); await handle_ask(false, media);
} else if (data.action == "load") { } else if (data.action == "load") {
inputCount.innerText = `Read data: ${formatFileSize(data.size)}`; inputCount.innerText = `Read data: ${formatFileSize(data.size)}`;

View File

@@ -37,7 +37,7 @@ EXTENSIONS_MAP: dict[str, str] = {
"flac": "audio/flac", "flac": "audio/flac",
"opus": "audio/opus", "opus": "audio/opus",
"ogg": "audio/ogg", "ogg": "audio/ogg",
"m4a": "audio/mp4", "m4a": "audio/m4a",
# Video # Video
"mkv": "video/x-matroska", "mkv": "video/x-matroska",
"webm": "video/webm", "webm": "video/webm",

View File

@@ -284,6 +284,10 @@ class SynthesizeData(HiddenResponse, JsonMixin):
self.provider = provider self.provider = provider
self.data = data self.data = data
class SuggestedFollowups(HiddenResponse):
def __init__(self, suggestions: list[str]):
self.suggestions = suggestions
class RequestLogin(HiddenResponse): class RequestLogin(HiddenResponse):
def __init__(self, label: str, login_url: str) -> None: def __init__(self, label: str, login_url: str) -> None:
"""Initialize with label and login URL.""" """Initialize with label and login URL."""

View File

@@ -13,6 +13,7 @@ try:
from duckduckgo_search import DDGS from duckduckgo_search import DDGS
from duckduckgo_search.exceptions import DuckDuckGoSearchException from duckduckgo_search.exceptions import DuckDuckGoSearchException
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
ddgs = DDGS()
has_requirements = True has_requirements = True
except ImportError: except ImportError:
has_requirements = False has_requirements = False
@@ -164,7 +165,7 @@ async def fetch_and_scrape(session: ClientSession, url: str, max_words: int = No
async def search(query: str, max_results: int = 5, max_words: int = 2500, backend: str = "auto", add_text: bool = True, timeout: int = 5, region: str = "wt-wt") -> SearchResults: async def search(query: str, max_results: int = 5, max_words: int = 2500, backend: str = "auto", add_text: bool = True, timeout: int = 5, region: str = "wt-wt") -> SearchResults:
if not has_requirements: if not has_requirements:
raise MissingRequirementsError('Install "duckduckgo-search" and "beautifulsoup4" package | pip install -U g4f[search]') raise MissingRequirementsError('Install "duckduckgo-search" and "beautifulsoup4" package | pip install -U g4f[search]')
with DDGS() as ddgs:
results = [] results = []
for result in ddgs.text( for result in ddgs.text(
query, query,