返回提交历史
Modified
g4f/Provider/Cloudflare.py
+1
-1
Modified
g4f/Provider/needs_auth/LMArena.py
+36
-37
Modified
g4f/Provider/openai/har_file.py
+4
-2
Modified
g4f/integration/pydantic_ai.py
+62
-5
Deleted
g4f/integration/uuid.py
+0
-1009
XFEstudio/gpt4free
Fix LMAreana provider
213e04ba
代码差异
5 个文件
+103
-1054
@@ -36,7 +36,7 @@ def clean_name(name: str) -> str:
36
36
class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
37
37
label = "Cloudflare AI"
38
38
url = "https://playground.ai.cloudflare.com"
39
working = has_curl_cffi
39
working = False
40
40
use_nodriver = True
41
41
active_by_default = True
42
42
api_endpoint = "https://playground.ai.cloudflare.com/api/inference"
@@ -1,11 +1,12 @@
1
1
from __future__ import annotations
2
2
3
import uuid
4
3
import json
5
4
import asyncio
6
5
import os
7
6
import requests
8
7
import json
8
import time
9
import secrets
9
10
10
11
try:
11
12
import curl_cffi
@@ -26,11 +27,26 @@ from ...requests import StreamSession, get_args_from_nodriver, raise_for_status,
26
27
from ...errors import ModelNotFoundError, CloudflareError, MissingAuthError, MissingRequirementsError
27
28
from ...providers.response import FinishReason, Usage, JsonConversation, ImageResponse, Reasoning, PlainTextResponse, JsonRequest
28
29
from ...tools.media import merge_media
29
from ...integration import uuid
30
30
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin
31
31
from ..helper import get_last_user_message
32
32
from ... import debug
33
33
34
def uuid7():
35
"""
36
Generate a UUIDv7 using Unix epoch (milliseconds since 1970-01-01)
37
matching the browser's implementation.
38
"""
39
timestamp_ms = int(time.time() * 1000)
40
rand_a = secrets.randbits(12)
41
rand_b = secrets.randbits(62)
42
43
uuid_int = timestamp_ms << 80
44
uuid_int |= (0x7000 | rand_a) << 64
45
uuid_int |= (0x8000000000000000 | rand_b)
46
47
hex_str = f"{uuid_int:032x}"
48
return f"{hex_str[0:8]}-{hex_str[8:12]}-{hex_str[12:16]}-{hex_str[16:20]}-{hex_str[20:32]}"
49
34
50
models = [
35
51
{'id': '812c93cc-5f88-4cff-b9ca-c11a26599b0e', 'publicName': 'qwen3-max-preview',
36
52
'capabilities': {'inputCapabilities': {'text': True}, 'outputCapabilities': {'text': True}},
@@ -485,7 +501,8 @@ class LMArena(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
485
501
label = "LMArena"
486
502
url = "https://lmarena.ai"
487
503
share_url = None
488
api_endpoint = "https://lmarena.ai/nextjs-api/stream/create-evaluation"
504
create_evaluation = "https://lmarena.ai/nextjs-api/stream/create-evaluation"
505
post_to_evaluation = "https://lmarena.ai/nextjs-api/stream/post-to-evaluation/{id}"
489
506
working = True
490
507
active_by_default = True
491
508
use_stream_timeout = False
@@ -637,21 +654,23 @@ class LMArena(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
637
654
else:
638
655
raise ModelNotFoundError(f"Model '{model}' is not supported by LMArena provider.")
639
656
640
evaluationSessionId = str(uuid.uuid7())
641
userMessageId = str(uuid.uuid7())
642
modelAMessageId = str(uuid.uuid7())
657
if conversation and getattr(conversation, "evaluationSessionId", None):
658
url = cls.post_to_evaluation.format(id=conversation.evaluationSessionId)
659
evaluationSessionId = conversation.evaluationSessionId
660
else:
661
url = cls.create_evaluation
662
evaluationSessionId = str(uuid7())
663
userMessageId = str(uuid7())
664
modelAMessageId = str(uuid7())
643
665
data = {
644
666
"id": evaluationSessionId,
645
667
"mode": "direct",
646
668
"modelAId": model_id,
647
669
"userMessageId": userMessageId,
648
670
"modelAMessageId": modelAMessageId,
649
"messages": [
650
{
651
"id": userMessageId,
652
"role": "user",
653
"content": prompt,
654
"experimental_attachments": [
671
"userMessage": {
672
"content": prompt,
673
"experimental_attachments": [
655
674
{
656
675
"name": name or os.path.basename(url),
657
676
"contentType": get_content_type(url),
@@ -660,33 +679,14 @@ class LMArena(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
660
679
for url, name in list(merge_media(media, messages))
661
680
if isinstance(url, str) and url.startswith("https://")
662
681
],
663
"parentMessageIds": [] if conversation is None else conversation.message_ids,
664
"participantPosition": "a",
665
"modelId": None,
666
"evaluationSessionId": evaluationSessionId,
667
"status": "pending",
668
"failureReason": None
669
},
670
{
671
"id": modelAMessageId,
672
"role": "assistant",
673
"content": "",
674
"experimental_attachments": [],
675
"parentMessageIds": [userMessageId],
676
"participantPosition": "a",
677
"modelId": model,
678
"evaluationSessionId": evaluationSessionId,
679
"status": "pending",
680
"failureReason": None
681
}
682
],
683
"modality": "image" if is_image_model else "chat"
682
},
683
"modality": "image" if is_image_model else "chat",
684
684
}
685
685
yield JsonRequest.from_dict(data)
686
686
try:
687
687
async with StreamSession(**args, timeout=timeout) as session:
688
688
async with session.post(
689
cls.api_endpoint,
689
url,
690
690
json=data,
691
691
proxy=proxy
692
692
) as response:
@@ -695,9 +695,7 @@ class LMArena(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
695
695
async for chunk in response.iter_lines():
696
696
line = chunk.decode()
697
697
yield PlainTextResponse(line)
698
if line.startswith("af:"):
699
yield JsonConversation(message_ids=[modelAMessageId])
700
elif line.startswith("a0:"):
698
if line.startswith("a0:"):
701
699
chunk = json.loads(line[3:])
702
700
if chunk == "hasArenaError":
703
701
raise ModelNotFoundError("LMArena Beta encountered an error: hasArenaError")
@@ -708,6 +706,7 @@ class LMArena(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
708
706
elif line.startswith("a2:"):
709
707
yield ImageResponse([image.get("image") for image in json.loads(line[3:])], prompt)
710
708
elif line.startswith("ad:"):
709
yield JsonConversation(evaluationSessionId=evaluationSessionId)
711
710
finish = json.loads(line[3:])
712
711
if "finishReason" in finish:
713
712
yield FinishReason(finish["finishReason"])
@@ -9,8 +9,10 @@ import uuid
9
9
import random
10
10
from urllib.parse import unquote
11
11
from copy import deepcopy
12
13
from .crypt import decrypt, encrypt
12
try:
13
from .crypt import decrypt, encrypt
14
except ImportError:
15
pass
14
16
from ...requests import StreamSession
15
17
from ...cookies import get_cookies_dir
16
18
from ...errors import NoValidHarFileError
@@ -4,16 +4,18 @@ from typing import Optional
4
4
from functools import partial
5
5
from dataclasses import dataclass, field
6
6
7
from pydantic_ai.models import Model, KnownModelName, infer_model
8
from pydantic_ai.models.openai import OpenAIModel, OpenAISystemPromptRole
7
from pydantic_ai import ModelResponsePart, ThinkingPart, ToolCallPart
8
from pydantic_ai.models import Model, ModelResponse, KnownModelName, infer_model
9
from pydantic_ai.models.openai import OpenAIChatModel, UnexpectedModelBehavior
10
from pydantic_ai.models.openai import OpenAISystemPromptRole, _CHAT_FINISH_REASON_MAP, _map_usage, _now_utc, number_to_datetime, split_content_into_text_and_thinking, replace
9
11
10
12
import pydantic_ai.models.openai
11
13
pydantic_ai.models.openai.NOT_GIVEN = None
12
14
13
from ..client import AsyncClient
15
from ..client import AsyncClient, ChatCompletion
14
16
15
17
@dataclass(init=False)
16
class AIModel(OpenAIModel):
18
class AIModel(OpenAIChatModel):
17
19
"""A model that uses the G4F API."""
18
20
19
21
client: AsyncClient = field(repr=False)
@@ -53,6 +55,61 @@ class AIModel(OpenAIModel):
53
55
if self._provider:
54
56
return f'g4f:{self._provider}:{self._model_name}'
55
57
return f'g4f:{self._model_name}'
58
59
def _process_response(self, response: ChatCompletion | str) -> ModelResponse:
60
"""Process a non-streamed response, and prepare a message to return."""
61
# Although the OpenAI SDK claims to return a Pydantic model (`ChatCompletion`) from the chat completions function:
62
# * it hasn't actually performed validation (presumably they're creating the model with `model_construct` or something?!)
63
# * if the endpoint returns plain text, the return type is a string
64
# Thus we validate it fully here.
65
if not isinstance(response, ChatCompletion):
66
raise UnexpectedModelBehavior('Invalid response from OpenAI chat completions endpoint, expected JSON data')
67
68
if response.created:
69
timestamp = number_to_datetime(response.created)
70
else:
71
timestamp = _now_utc()
72
response.created = int(timestamp.timestamp())
73
74
# Workaround for local Ollama which sometimes returns a `None` finish reason.
75
if response.choices and (choice := response.choices[0]) and choice.finish_reason is None: # pyright: ignore[reportUnnecessaryComparison]
76
choice.finish_reason = 'stop'
77
78
choice = response.choices[0]
79
items: list[ModelResponsePart] = []
80
81
# The `reasoning` field is only present in gpt-oss via Ollama and OpenRouter.
82
# - https://cookbook.openai.com/articles/gpt-oss/handle-raw-cot#chat-completions-api
83
# - https://openrouter.ai/docs/use-cases/reasoning-tokens#basic-usage-with-reasoning-tokens
84
if reasoning := getattr(choice.message, 'reasoning', None):
85
items.append(ThinkingPart(id='reasoning', content=reasoning, provider_name=self.system))
86
87
# NOTE: We don't currently handle OpenRouter `reasoning_details`:
88
# - https://openrouter.ai/docs/use-cases/reasoning-tokens#preserving-reasoning-blocks
89
# If you need this, please file an issue.
90
91
if choice.message.content:
92
items.extend(
93
(replace(part, id='content', provider_name=self.system) if isinstance(part, ThinkingPart) else part)
94
for part in split_content_into_text_and_thinking(choice.message.content, self.profile.thinking_tags)
95
)
96
if choice.message.tool_calls is not None:
97
for c in choice.message.tool_calls:
98
items.append(ToolCallPart(c.get("function").get("name"), c.get("function").get("arguments"), tool_call_id=c.get("id")))
99
100
raw_finish_reason = choice.finish_reason
101
finish_reason = _CHAT_FINISH_REASON_MAP.get(raw_finish_reason)
102
103
return ModelResponse(
104
parts=items,
105
usage=_map_usage(response, self._provider, "", self._model_name),
106
model_name=response.model,
107
timestamp=timestamp,
108
provider_details=None,
109
provider_response_id=response.id,
110
provider_name=self._provider,
111
finish_reason=finish_reason,
112
)
56
113
57
114
def new_infer_model(model: Model | KnownModelName, api_key: str = None) -> Model:
58
115
if isinstance(model, Model):
@@ -69,4 +126,4 @@ def patch_infer_model(api_key: str | None = None):
69
126
import pydantic_ai.models
70
127
71
128
pydantic_ai.models.infer_model = partial(new_infer_model, api_key=api_key)
72
pydantic_ai.models.AIModel = AIModel
129
pydantic_ai.models.OpenAIChatModel = AIModel