MeUtils 2025.4.9.10.39.6__py3-none-any.whl → 2025.4.11.17.37.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {MeUtils-2025.4.9.10.39.6.dist-info → MeUtils-2025.4.11.17.37.3.dist-info}/METADATA +266 -266
  2. {MeUtils-2025.4.9.10.39.6.dist-info → MeUtils-2025.4.11.17.37.3.dist-info}/RECORD +40 -37
  3. examples/_openaisdk/openai_google.py +38 -31
  4. meutils/apis/{google_apis → google}/audios.py +5 -2
  5. meutils/apis/google/chat.py +372 -0
  6. meutils/apis/google/files.py +29 -0
  7. meutils/apis/{google_apis → google}/google2openai.py +8 -4
  8. meutils/apis/google/images.py +27 -0
  9. meutils/apis/{google_apis → google}/search.py +7 -5
  10. meutils/apis/jimeng/images.py +41 -8
  11. meutils/apis/search/metaso.py +2 -2
  12. meutils/apis/siliconflow/images.py +5 -3
  13. meutils/caches/acache.py +1 -1
  14. meutils/common.py +1 -0
  15. meutils/data/VERSION +1 -1
  16. meutils/decorators/catch.py +0 -8
  17. meutils/decorators/common.py +86 -19
  18. meutils/decorators/contextmanagers.py +103 -14
  19. meutils/decorators/demo.py +76 -14
  20. meutils/io/files_utils.py +2 -3
  21. meutils/io/openai_files.py +11 -6
  22. meutils/llm/check_utils.py +20 -1
  23. meutils/llm/openai_polling/__init__.py +11 -0
  24. meutils/llm/openai_polling/chat.py +45 -0
  25. meutils/{apis/google_apis → llm/openai_polling}/images.py +4 -2
  26. meutils/llm/openai_utils/common.py +1 -1
  27. meutils/notice/feishu.py +5 -2
  28. meutils/schemas/image_types.py +26 -3
  29. meutils/schemas/oneapi/common.py +12 -0
  30. meutils/schemas/openai_types.py +10 -0
  31. meutils/serving/fastapi/dependencies/__init__.py +4 -1
  32. meutils/serving/fastapi/dependencies/auth.py +10 -6
  33. meutils/serving/fastapi/exceptions/http_error.py +2 -2
  34. meutils/str_utils/__init__.py +30 -2
  35. meutils/apis/google_apis/common.py +0 -243
  36. meutils/apis/google_apis/files.py +0 -19
  37. {MeUtils-2025.4.9.10.39.6.dist-info → MeUtils-2025.4.11.17.37.3.dist-info}/LICENSE +0 -0
  38. {MeUtils-2025.4.9.10.39.6.dist-info → MeUtils-2025.4.11.17.37.3.dist-info}/WHEEL +0 -0
  39. {MeUtils-2025.4.9.10.39.6.dist-info → MeUtils-2025.4.11.17.37.3.dist-info}/entry_points.txt +0 -0
  40. {MeUtils-2025.4.9.10.39.6.dist-info → MeUtils-2025.4.11.17.37.3.dist-info}/top_level.txt +0 -0
  41. /meutils/apis/{google_apis → google}/__init__.py +0 -0
  42. /meutils/apis/{google_apis → google}/gemini_sdk.py +0 -0
@@ -386,6 +386,11 @@ MODEL_RATIO = {
386
386
  "grok-3-reasoner": 2,
387
387
  "grok-3-deepersearch": 2,
388
388
 
389
+ "grok-3-beta": 1.5,
390
+ "grok-3-fast-beta": 2.5,
391
+ "grok-3-mini-beta": 0.15,
392
+ "grok-3-mini-fast-beta": 0.3,
393
+
389
394
  # 定制
390
395
  "lingxi-all": 1,
391
396
 
@@ -681,6 +686,7 @@ MODEL_RATIO = {
681
686
  "gemini-2.0-pro": 1.25,
682
687
  "gemini-2.0-pro-exp-02-05": 1.25,
683
688
  "gemini-2.5-pro-exp-03-25": 1.5,
689
+ "gemini-2.5-pro-preview-03-25": 0.625,
684
690
 
685
691
  "gemini-2.0-flash-thinking-exp": 1,
686
692
  "gemini-2.0-flash-thinking-exp-1219": 1,
@@ -816,6 +822,11 @@ COMPLETION_RATIO = {
816
822
  "grok-3-reasoner": 5,
817
823
  "grok-3-deepersearch": 5,
818
824
 
825
+ "grok-3-beta": 5,
826
+ "grok-3-fast-beta": 5,
827
+ "grok-3-mini-beta": 5 / 3,
828
+ "grok-3-mini-fast-beta": 4 / 0.6,
829
+
819
830
  "claude-3-5-haiku-20241022": 5,
820
831
  "anthropic/claude-3-5-haiku-20241022:beta": 5,
821
832
 
@@ -905,6 +916,7 @@ COMPLETION_RATIO = {
905
916
  "gemini-2.0-pro": 5,
906
917
  "gemini-2.0-pro-exp-02-05": 5,
907
918
  "gemini-2.5-pro-exp-03-25": 5,
919
+ "gemini-2.5-pro-preview-03-25": 8,
908
920
 
909
921
  "gemma2-9b-it": 4,
910
922
  "gemma2-27b-it": 4,
@@ -121,6 +121,14 @@ class CompletionRequest(BaseModel):
121
121
  def __init__(self, **kwargs):
122
122
  super().__init__(**kwargs)
123
123
 
124
+ @cached_property
125
+ def system_instruction(self):
126
+ # [{'role': 'system', 'content': [{"type": "text", "text": ""}]]
127
+
128
+ if message := self.messages and self.messages[0]:
129
+ if message.get("role") == "system":
130
+ return str(message.get("content", "")) or None
131
+
124
132
  @cached_property
125
133
  def last_message(self):
126
134
  return self.messages and self.messages[-1]
@@ -159,7 +167,9 @@ class CompletionRequest(BaseModel):
159
167
  """最新一轮的 user url 列表"""
160
168
  content_types = {
161
169
  "image_url",
170
+
162
171
  "file", "file_url",
172
+
163
173
  "audio_url", "input_audio",
164
174
 
165
175
  "video_url",
@@ -6,5 +6,8 @@
6
6
  # @Author : betterme
7
7
  # @WeChat : meutils
8
8
  # @Software : PyCharm
9
- # @Description :
9
+ # @Description :
10
10
 
11
+
12
+ from meutils.serving.fastapi.dependencies.auth import get_bearer_token
13
+ from meutils.serving.fastapi.dependencies.headers import get_headers
@@ -33,15 +33,18 @@ async def get_bearer_token(
33
33
  return None
34
34
 
35
35
  token = auth.credentials
36
- if token.startswith('redis:'): # 初始化吧,太长?
36
+ if token.startswith('redis:'): # redis里按序轮询
37
37
  if "feishu.cn" in token:
38
38
  feishu_url = token.removeprefix("redis:")
39
- token = await get_next_token(feishu_url) # 初始化redis
39
+ token = await get_next_token(feishu_url) # todo: 摆脱 feishu
40
40
 
41
- elif ',' in token: # todo: 初始化redis
42
- pass
41
+ # logger.debug(token)
43
42
 
44
- elif ',' in token: # 分隔符
43
+ else: # redis:token1,token2
44
+ tokens = token.removeprefix("redis:").split(',') # todo: 初始化redis
45
+ token = np.random.choice(tokens)
46
+
47
+ elif ',' in token: # 内存里随机轮询
45
48
  token = np.random.choice(token.split(','))
46
49
 
47
50
  return token
@@ -71,4 +74,5 @@ async def get_bearer_token_for_oneapi(
71
74
  if __name__ == '__main__':
72
75
  from meutils.pipe import *
73
76
 
74
- arun(get_bearer_token(HTTPAuthorizationCredentials(scheme="Bearer", credentials="123")))
77
+ arun(get_bearer_token(HTTPAuthorizationCredentials(scheme="Bearer",
78
+ credentials="redis:https://xchatllm.feishu.cn/sheets/Bmjtst2f6hfMqFttbhLcdfRJnNf?sheet=rWMtht")))
@@ -10,7 +10,7 @@ from starlette.responses import JSONResponse
10
10
 
11
11
  from fastapi.exceptions import RequestValidationError, HTTPException
12
12
 
13
- from meutils.notice.feishu import send_message_for_httpexception as send_message
13
+ from meutils.notice.feishu import send_message_for_http as send_message
14
14
 
15
15
 
16
16
  async def http_error_handler(_: Request, exc: HTTPException) -> JSONResponse:
@@ -60,7 +60,7 @@ async def chatfire_api_exception_handler(request: Request, exc: Exception):
60
60
  )
61
61
 
62
62
  # send_message
63
- content_detail = f"{traceback.format_exc()}"
63
+ content_detail = f"{traceback.format_exc()}" # 是不是最后几行就可以了
64
64
  #
65
65
  # from meutils.pipe import logger
66
66
  # logger.debug(content_detail)
@@ -11,12 +11,32 @@
11
11
 
12
12
  from meutils.pipe import *
13
13
  # from meutils.str_utils.translater import translater
14
+ from meutils.str_utils.regular_expression import parse_url
15
+
14
16
  from meutils.request_utils.crawler import Crawler
15
17
  from urllib.parse import urlencode, parse_qs, parse_qsl, quote_plus, unquote_plus, urljoin
16
18
 
17
19
  query_params2json = lambda q: dict(parse_qsl(q))
18
20
 
19
21
 
22
+ def parse_prompt(prompt, only_first_url: bool = True):
23
+ if not only_first_url and (urls := parse_url(prompt)):
24
+ prompt = reduce(lambda prompt, url: prompt.replace(url, " "), urls)
25
+ return urls, prompt
26
+
27
+ if prompt.startswith('http') and (prompts := prompt.split(maxsplit=1)): # 单 url
28
+ if len(prompts) == 2:
29
+ return prompts
30
+ else:
31
+ return prompts + [' ']
32
+
33
+ elif "http" in prompt and (urls := parse_url(prompt)):
34
+ return urls[0], prompt.replace(urls[0], "")
35
+
36
+ else:
37
+ return None, prompt
38
+
39
+
20
40
  def json2query_params(js: dict = None, url='') -> str:
21
41
  js = js or {}
22
42
  js = {k: v for k, v in js.items() if v}
@@ -200,8 +220,16 @@ if __name__ == '__main__':
200
220
 
201
221
  # print(query_params2json("act=get&uid=134597&vkey=07A1D82FDDE1E96DB5CEF4EF12C8125F&num=1&time=30&plat=1&re=1&type=0&so=1&ow=1&spl=1&addr=&db=1"))
202
222
 
203
- print(half2all('⽉月'))
204
- print(all2half('⽉月'))
223
+ # print(half2all('⽉月'))
224
+ # print(all2half('⽉月'))
225
+
226
+ print(parse_prompt(" hi", only_first_url=False))
227
+
228
+ print(parse_prompt("https://www.hao123.com/ hi"))
229
+ print(parse_prompt("https://www.hao123.com/ hi" * 2))
230
+ print(parse_prompt("hi https://www.hao123.com/ "))
231
+
232
+ print(parse_prompt("https://www.hao123.com/ hi" * 2, only_first_url=False))
205
233
 
206
234
  # import chardet
207
235
  #
@@ -1,243 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- # @Project : AI. @by PyCharm
4
- # @File : common
5
- # @Time : 2025/4/2 13:03
6
- # @Author : betterme
7
- # @WeChat : meutils
8
- # @Software : PyCharm
9
- # @Description : https://ai.google.dev/gemini-api/docs/openai?hl=zh-cn
10
-
11
- # https://googleapis.github.io/python-genai/genai.html#module-genai.models
12
-
13
-
14
- from meutils.pipe import *
15
- from meutils.llm.clients import AsyncOpenAI
16
- from meutils.io.files_utils import to_url, to_bytes, guess_mime_type
17
- from meutils.str_utils.regular_expression import parse_url
18
-
19
- from meutils.schemas.openai_types import chat_completion, chat_completion_chunk, CompletionRequest, CompletionUsage
20
-
21
- from meutils.config_utils.lark_utils import get_next_token_for_polling
22
- from google import genai
23
- from google.genai import types
24
- from google.genai.types import HttpOptions, GenerateContentConfig, Content, HarmCategory, HarmBlockThreshold, Part
25
- from google.genai.types import UserContent, ModelContent
26
-
27
- FEISHU_URL = "https://xchatllm.feishu.cn/sheets/Bmjtst2f6hfMqFttbhLcdfRJnNf?sheet=MJw6hI"
28
-
29
- """
30
- Gemini 1.5 Pro 和 1.5 Flash 最多支持 3,600 个文档页面。文档页面必须采用以下文本数据 MIME 类型之一:
31
-
32
- PDF - application/pdf
33
- JavaScript - application/x-javascript、text/javascript
34
- Python - application/x-python、text/x-python
35
- TXT - text/plain
36
- HTML - text/html
37
- CSS - text/css
38
- Markdown - text/md
39
- CSV - text/csv
40
- XML - text/xml
41
- RTF - text/rtf
42
-
43
- - 小文件
44
- - 大文件: 需要等待处理
45
- """
46
-
47
-
48
- class GeminiClient(object):
49
- def __init__(self, api_key: Optional[str] = None):
50
- self.api_key = api_key # or await get_next_token_for_polling(feishu_url=FEISHU_URL, from_redis=True)
51
- self.base_url = "https://all.chatfire.cc/genai"
52
-
53
- async def create(self, request: CompletionRequest):
54
- client = await self.get_client()
55
-
56
- if any(i in request.model for i in ("image",)):
57
- messages = await self.to_image_messages(request)
58
-
59
- if len(messages) > 1:
60
- history = messages[:-1]
61
- message = messages[-1].parts
62
- else:
63
- history = []
64
- message = messages[-1].parts
65
-
66
- chat = client.aio.chats.create( # todo: system_instruction
67
- model=request.model,
68
- config=GenerateContentConfig(
69
- response_modalities=['Text', 'Image'],
70
- ),
71
- history=history
72
- )
73
-
74
- logger.debug(message)
75
-
76
- # message = [
77
- # Part.from_text(text="画条狗")
78
- # ]
79
-
80
- for i in range(5):
81
- try:
82
- chunks = await chat.send_message_stream(message)
83
- async for chunk in chunks:
84
-
85
- if chunk.candidates:
86
- parts = chunk.candidates[0].content.parts or []
87
- for part in parts:
88
- # logger.debug(part)
89
- if part.text:
90
- yield part.text
91
-
92
- if part.inline_data:
93
- image_url = await to_url(
94
- part.inline_data.data,
95
- mime_type=part.inline_data.mime_type
96
- )
97
- yield f"![image_url]({image_url})"
98
- break
99
-
100
- except Exception as e:
101
- logger.debug(f"重试{i}: {e}")
102
- if "The model is overloaded." in str(e):
103
- await asyncio.sleep(1)
104
- continue
105
- else:
106
- yield e
107
- raise e
108
-
109
- async def to_image_messages(self, request: CompletionRequest):
110
- # 两轮即可连续编辑图片
111
-
112
- messages = []
113
- for m in request.messages:
114
- contents = m.get("content")
115
- if m.get("role") == "assistant":
116
- assistant_content = str(contents)
117
- if urls := parse_url(assistant_content): # assistant
118
- datas = await asyncio.gather(*map(to_bytes, urls))
119
-
120
- parts = [
121
- Part.from_bytes(
122
- data=data,
123
- mime_type="image/png"
124
- )
125
- for data in datas
126
- ]
127
- parts += [
128
- Part.from_text(
129
- text=request.last_assistant_content
130
- ),
131
- ]
132
- messages.append(ModelContent(parts=parts))
133
-
134
- elif m.get("role") == "user":
135
- if isinstance(contents, list):
136
- parts = []
137
- for content in contents:
138
- if content.get("type") == "image_url":
139
- image_url = content.get("image_url", {}).get("url")
140
- data = await to_bytes(image_url)
141
-
142
- parts += [
143
- Part.from_bytes(data=data, mime_type="image/png")
144
- ]
145
-
146
- elif content.get("type") == "text":
147
- text = content.get("text")
148
- if text.startswith('http'):
149
- image_url, text = text.split(maxsplit=1)
150
- data = await to_bytes(image_url)
151
-
152
- parts += [
153
- Part.from_bytes(data=data, mime_type="image/png"),
154
- Part.from_text(text=text)
155
- ]
156
- else:
157
- parts += [
158
- Part.from_text(text=text)
159
- ]
160
-
161
- messages.append(UserContent(parts=parts))
162
-
163
- else: # str
164
- if str(contents).startswith('http'): # 修正提问格式, 兼容 url
165
- image_url, text = str(contents).split(maxsplit=1)
166
- data = await to_bytes(image_url)
167
- parts = [
168
- Part.from_bytes(data=data, mime_type="image/png"),
169
- Part.from_text(text=text)
170
- ]
171
- else:
172
- parts = [
173
- Part.from_text(text=str(contents)),
174
- ]
175
- messages.append(UserContent(parts=parts))
176
-
177
- return messages
178
-
179
- async def upload(self, files: Union[str, List[str]]): # => openai files
180
- client = await self.get_client()
181
-
182
- if isinstance(files, list):
183
- return await asyncio.gather(*map(self.upload, files))
184
-
185
- file_config = {"name": f"{shortuuid.random().lower()}", "mime_type": guess_mime_type(files)}
186
- return await client.aio.files.upload(file=io.BytesIO(await to_bytes(files)), config=file_config)
187
-
188
- @alru_cache()
189
- async def get_client(self):
190
- api_key = self.api_key or await get_next_token_for_polling(feishu_url=FEISHU_URL, from_redis=True)
191
-
192
- logger.info(f"GeminiClient: {api_key}")
193
-
194
- return genai.Client(
195
- api_key=api_key,
196
- http_options=HttpOptions(
197
- base_url=self.base_url
198
- )
199
- )
200
-
201
-
202
- if __name__ == '__main__':
203
- file = "https://oss.ffire.cc/files/kling_watermark.png"
204
-
205
- api_key = os.getenv("GOOGLE_API_KEY")
206
-
207
- # arun(GeminiClient(api_key=api_key).upload(file))
208
- # arun(GeminiClient(api_key=api_key).upload([file] * 2))
209
- # arun(GeminiClient(api_key=api_key).create())
210
-
211
- content = [
212
-
213
- {"type": "text", "text": "https://oss.ffire.cc/files/nsfw.jpg 移除右下角的水印"},
214
-
215
- # {"type": "text", "text": "总结下"},
216
- # {"type": "image_url", "image_url": {"url": url}},
217
-
218
- # {"type": "video_url", "video_url": {"url": url}}
219
-
220
- ]
221
-
222
- # content = "https://oss.ffire.cc/files/nsfw.jpg 移除右下角的水印"
223
-
224
- #
225
- request = CompletionRequest(
226
- # model="qwen-turbo-2024-11-01",
227
- # model="gemini-all",
228
- model="gemini-2.0-flash-exp-image-generation",
229
- # model="qwen-plus-latest",
230
-
231
- messages=[
232
- {
233
- 'role': 'user',
234
-
235
- 'content': content
236
- },
237
-
238
- ],
239
- stream=True,
240
- )
241
-
242
- # arun(GeminiClient(api_key=api_key).to_image_messages(request))
243
- arun(GeminiClient(api_key=api_key).create(request))
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- # @Project : AI. @by PyCharm
4
- # @File : files
5
- # @Time : 2025/4/2 10:40
6
- # @Author : betterme
7
- # @WeChat : meutils
8
- # @Software : PyCharm
9
- # @Description :
10
-
11
- from meutils.pipe import *
12
-
13
-
14
- file = "/Users/betterme/PycharmProjects/AI/QR.png"
15
- #
16
- # file_object = client.files.upload(file=file)
17
- # prompt = "一句话总结"
18
-
19
- file_object = client.aio.files.upload(file=file)
File without changes
File without changes