XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 1
返回提交历史

XFEstudio/gpt4free

Fix LMAreana provider

213e04ba
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

5 个文件 +103 -1054
Modified g4f/Provider/Cloudflare.py +1 -1
@@ -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"
Modified g4f/Provider/needs_auth/LMArena.py +36 -37
@@ -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"])
Modified g4f/Provider/openai/har_file.py +4 -2
@@ -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
Modified g4f/integration/pydantic_ai.py +62 -5
@@ -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