vectorvein 0.1.40__py3-none-any.whl → 0.1.42__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.
@@ -4,16 +4,14 @@ import re
4
4
  import json
5
5
  from math import ceil
6
6
  from typing import Iterable
7
+
7
8
  import httpx
8
9
  import tiktoken
9
10
  from anthropic import Anthropic
10
- from qwen_tokenizer import get_tokenizer
11
- from deepseek_tokenizer import deepseek_tokenizer
12
11
 
13
12
  from ..settings import settings
14
13
  from ..utilities.retry import Retry
15
14
  from ..types.enums import BackendType
16
- from ..utilities.media_processing import ImageProcessor
17
15
  from ..types.llm_parameters import (
18
16
  NotGiven,
19
17
  NOT_GIVEN,
@@ -116,7 +114,20 @@ def convert_type(value, value_type):
116
114
  return value # 如果类型未知,返回原始值
117
115
 
118
116
 
119
- def get_token_counts(text: str | dict, model: str = "") -> int:
117
+ def get_token_counts(text: str | dict, model: str = "", use_token_server_first: bool = False) -> int:
118
+ if use_token_server_first and settings.token_server is not None:
119
+ _, response = (
120
+ Retry(httpx.post)
121
+ .args(url=settings.token_server, json={"text": text, "model": model}, timeout=None)
122
+ .retry_times(5)
123
+ .sleep_time(1)
124
+ .run()
125
+ )
126
+ if response is None:
127
+ return 1000
128
+ result = response.json()
129
+ return result["total_tokens"]
130
+
120
131
  if not isinstance(text, str):
121
132
  text = str(text)
122
133
  if model == "gpt-3.5-turbo":
@@ -128,6 +139,8 @@ def get_token_counts(text: str | dict, model: str = "") -> int:
128
139
  if len(model_setting.endpoints) == 0:
129
140
  return int(len(text) / 1.33)
130
141
  endpoint_id = model_setting.endpoints[0]
142
+ if isinstance(endpoint_id, dict):
143
+ endpoint_id = endpoint_id["endpoint_id"]
131
144
  endpoint = settings.get_endpoint(endpoint_id)
132
145
  tokenize_url = "https://api.minimax.chat/v1/tokenize"
133
146
  headers = {"Authorization": f"Bearer {endpoint.api_key}", "Content-Type": "application/json"}
@@ -156,6 +169,8 @@ def get_token_counts(text: str | dict, model: str = "") -> int:
156
169
  if len(model_setting.endpoints) == 0:
157
170
  return len(get_gpt_35_encoding().encode(text))
158
171
  endpoint_id = model_setting.endpoints[0]
172
+ if isinstance(endpoint_id, dict):
173
+ endpoint_id = endpoint_id["endpoint_id"]
159
174
  endpoint = settings.get_endpoint(endpoint_id)
160
175
  tokenize_url = "https://api.moonshot.cn/v1/tokenizers/estimate-token-count"
161
176
  headers = {"Content-Type": "application/json", "Authorization": f"Bearer {endpoint.api_key}"}
@@ -181,6 +196,8 @@ def get_token_counts(text: str | dict, model: str = "") -> int:
181
196
  if len(model_setting.endpoints) == 0:
182
197
  return len(get_gpt_35_encoding().encode(text))
183
198
  endpoint_id = model_setting.endpoints[0]
199
+ if isinstance(endpoint_id, dict):
200
+ endpoint_id = endpoint_id["endpoint_id"]
184
201
  endpoint = settings.get_endpoint(endpoint_id)
185
202
  url = f"{endpoint.api_base}/models/{model_setting.id}:countTokens"
186
203
  params = {"key": endpoint.api_key}
@@ -206,8 +223,12 @@ def get_token_counts(text: str | dict, model: str = "") -> int:
206
223
  elif model.startswith("claude"):
207
224
  return Anthropic().count_tokens(text)
208
225
  elif model.startswith("deepseek"):
226
+ from deepseek_tokenizer import deepseek_tokenizer
227
+
209
228
  return len(deepseek_tokenizer.encode(text))
210
229
  elif model.startswith("qwen"):
230
+ from qwen_tokenizer import get_tokenizer
231
+
211
232
  qwen_tokenizer = get_tokenizer(model)
212
233
  return len(qwen_tokenizer.encode(text))
213
234
  elif model.startswith("stepfun"):
@@ -215,6 +236,8 @@ def get_token_counts(text: str | dict, model: str = "") -> int:
215
236
  if len(model_setting.endpoints) == 0:
216
237
  return len(get_gpt_35_encoding().encode(text))
217
238
  endpoint_id = model_setting.endpoints[0]
239
+ if isinstance(endpoint_id, dict):
240
+ endpoint_id = endpoint_id["endpoint_id"]
218
241
  endpoint = settings.get_endpoint(endpoint_id)
219
242
  tokenize_url = "https://api.stepfun.com/v1/token/count"
220
243
  headers = {"Content-Type": "application/json", "Authorization": f"Bearer {endpoint.api_key}"}
@@ -372,6 +395,8 @@ def cutoff_messages(
372
395
 
373
396
 
374
397
  def format_image_message(image: str, backend: BackendType = BackendType.OpenAI) -> dict:
398
+ from ..utilities.media_processing import ImageProcessor
399
+
375
400
  image_processor = ImageProcessor(image_source=image)
376
401
  if backend == BackendType.OpenAI:
377
402
  return {
@@ -0,0 +1,47 @@
1
+ import uvicorn
2
+ from pydantic import BaseModel
3
+ from fastapi import FastAPI, HTTPException
4
+
5
+ from ..settings import settings
6
+ from ..chat_clients.utils import get_token_counts
7
+
8
+ token_server = FastAPI()
9
+
10
+
11
+ class TokenCountRequest(BaseModel):
12
+ text: str | dict
13
+ model: str = ""
14
+
15
+
16
+ @token_server.post("/count_tokens")
17
+ async def count_tokens(request: TokenCountRequest):
18
+ try:
19
+ token_count = get_token_counts(request.text, request.model, use_token_server_first=False)
20
+ return {"total_tokens": token_count}
21
+ except Exception as e:
22
+ raise HTTPException(status_code=500, detail=str(e))
23
+
24
+
25
+ def run_token_server(host: str | None = None, port: int | None = None):
26
+ """
27
+ 启动一个简单的HTTP服务器来处理token计数请求。参数均留空则使用 settings.token_server 的配置。
28
+
29
+ 参数:
30
+ host (str): 服务器主机地址。
31
+ port (int): 服务器端口。
32
+ """
33
+ if host is None or port is None:
34
+ token_server_url = settings.token_server
35
+ if token_server_url is None:
36
+ raise ValueError("Token server is not enabled.")
37
+
38
+ _host, _port = token_server_url.split(":")
39
+ else:
40
+ _host = host
41
+ _port = port
42
+
43
+ uvicorn.run(token_server, host=_host, port=int(_port))
44
+
45
+
46
+ if __name__ == "__main__":
47
+ run_token_server()
@@ -1,6 +1,6 @@
1
1
  # @Author: Bi Ying
2
2
  # @Date: 2024-07-27 00:30:56
3
- from typing import List, Dict
3
+ from typing import List, Dict, Optional
4
4
 
5
5
  from pydantic import BaseModel, Field
6
6
 
@@ -13,6 +13,7 @@ class Settings(BaseModel):
13
13
  endpoints: List[EndpointSetting] = Field(
14
14
  default_factory=list, description="Available endpoints for the LLM service."
15
15
  )
16
+ token_server: Optional[str] = Field(default=None, description="Token server address. Format: host:port")
16
17
 
17
18
  anthropic: BackendSettings = Field(default_factory=BackendSettings, description="Anthropic models settings.")
18
19
  deepseek: BackendSettings = Field(default_factory=BackendSettings, description="Deepseek models settings.")
@@ -7,64 +7,142 @@ from functools import cached_property
7
7
 
8
8
  import httpx
9
9
  from PIL import Image
10
-
10
+ from PIL.ImageFile import ImageFile
11
11
 
12
12
 
13
13
  class ImageProcessor:
14
- def __init__(self, image_source: Image.Image | str | Path, max_size: int | None = 5 * 1024 * 1024):
14
+ def __init__(
15
+ self,
16
+ image_source: Image.Image | str | Path,
17
+ max_size: int | None = 5 * 1024 * 1024,
18
+ max_width: int | None = None,
19
+ max_height: int | None = None,
20
+ ):
15
21
  self.image_source = image_source
16
22
  if isinstance(image_source, (Image.Image, Path)):
17
23
  self.is_local = True
18
24
  else:
19
25
  self.is_local = not image_source.startswith("http")
20
26
  self.max_size = max_size
27
+ self.max_width = max_width
28
+ self.max_height = max_height
21
29
  self._image = self._load_image()
30
+ self._image_format = self._image.format or "JPEG"
31
+ self._cached_bytes = None
32
+ self._cached_base64_image = None
22
33
 
23
34
  def _load_image(self):
24
- if not self.is_local:
35
+ if not self.is_local and isinstance(self.image_source, str):
25
36
  image_url = self.image_source
26
37
  response = httpx.get(image_url)
27
38
  return Image.open(BytesIO(response.content))
28
- else:
39
+ elif isinstance(self.image_source, Path):
29
40
  return Image.open(self.image_source)
41
+ elif isinstance(self.image_source, Image.Image):
42
+ return self.image_source
43
+ else:
44
+ raise ValueError(f"Unsupported image source type: {type(self.image_source)}")
45
+
46
+ def _resize_image(
47
+ self,
48
+ img: ImageFile | Image.Image,
49
+ max_size: int | None = None,
50
+ max_width: int | None = None,
51
+ max_height: int | None = None,
52
+ ):
53
+ img_bytes = BytesIO()
54
+ image_format = img.format or "JPEG"
55
+ _img = img.copy()
56
+ _img.save(img_bytes, format=image_format, optimize=True)
57
+
58
+ if max_width is not None and _img.width > max_width:
59
+ new_size = (max_width, int(max_width * _img.height / _img.width))
60
+ _img = _img.resize(new_size, Image.Resampling.LANCZOS)
61
+
62
+ if max_height is not None and _img.height > max_height:
63
+ new_size = (int(max_height * _img.width / _img.height), max_height)
64
+ _img = _img.resize(new_size, Image.Resampling.LANCZOS)
30
65
 
31
- def _resize_image(self, img, max_size):
32
66
  img_bytes = BytesIO()
33
- img.save(img_bytes, format=img.format, optimize=True)
67
+ _img.save(img_bytes, format=image_format, optimize=True)
34
68
 
35
- if img_bytes.getbuffer().nbytes <= max_size:
69
+ if max_size is not None and img_bytes.getbuffer().nbytes <= max_size:
36
70
  return img_bytes
37
71
 
38
- original_size = img.size
72
+ original_size = _img.size
39
73
  scale_factor = 0.9
40
74
 
41
75
  while True:
42
76
  new_size = (int(original_size[0] * scale_factor), int(original_size[1] * scale_factor))
43
- img_resized = img.resize(new_size, Image.Resampling.LANCZOS)
77
+ img_resized = _img.resize(new_size, Image.Resampling.LANCZOS)
44
78
 
45
79
  img_bytes_resized = BytesIO()
46
- img_resized.save(img_bytes_resized, format=img.format, optimize=True)
80
+ img_resized.save(img_bytes_resized, format=image_format, optimize=True)
47
81
 
48
- if img_bytes_resized.getbuffer().nbytes <= max_size:
82
+ if max_size is not None and img_bytes_resized.getbuffer().nbytes <= max_size:
49
83
  return img_bytes_resized
50
84
 
51
85
  scale_factor -= 0.1
52
86
  if scale_factor < 0.1:
53
87
  return img_bytes_resized
54
88
 
55
- @cached_property
89
+ @property
90
+ def bytes(self):
91
+ if self._cached_bytes is not None:
92
+ return self._cached_bytes
93
+ if self.max_size is None and self.max_width is None and self.max_height is None:
94
+ if isinstance(self._image, Image.Image):
95
+ img_bytes = BytesIO()
96
+
97
+ # 检查图像是否有透明通道
98
+ has_transparency = self._image.mode in ("RGBA", "LA") or (
99
+ self._image.mode == "P" and "transparency" in self._image.info
100
+ )
101
+
102
+ if has_transparency:
103
+ # 如果有透明通道,使用PNG格式
104
+ save_format = "PNG"
105
+ self._image_format = "PNG"
106
+ else:
107
+ # 如果没有透明通道,使用原始格式或默认为JPEG
108
+ save_format = self._image.format or self._image_format or "JPEG"
109
+
110
+ # 如果图像模式不是RGB(例如RGBA),转换为RGB
111
+ if self._image.mode != "RGB":
112
+ self._image = self._image.convert("RGB")
113
+
114
+ self._image.save(img_bytes, format=save_format, optimize=True)
115
+ self._cached_bytes = img_bytes.getvalue()
116
+ return self._cached_bytes
117
+ elif isinstance(self._image, BytesIO):
118
+ self._cached_bytes = self._image.getvalue()
119
+ return self._cached_bytes
120
+ elif isinstance(self._image, ImageFile):
121
+ if self._image.fp is None:
122
+ raise ValueError("Image file is not open")
123
+ self._cached_bytes = self._image.fp.read()
124
+ return self._cached_bytes
125
+
126
+ self._cached_bytes = self._image.getvalue()
127
+ return self._cached_bytes
128
+
129
+ img_bytes_resized = self._resize_image(self._image, self.max_size, self.max_width, self.max_height)
130
+ return img_bytes_resized.getvalue()
131
+
132
+ @property
56
133
  def base64_image(self):
57
- if self.max_size is None:
58
- return base64.b64encode(self._image.getvalue()).decode()
134
+ if self.max_size is None and self.max_width is None and self.max_height is None:
135
+ self._cached_base64_image = base64.b64encode(self.bytes).decode()
136
+ return self._cached_base64_image
59
137
 
60
- img_bytes_resized = self._resize_image(self._image, self.max_size)
61
- return base64.b64encode(img_bytes_resized.getvalue()).decode()
138
+ img_bytes_resized = self._resize_image(self._image, self.max_size, self.max_width, self.max_height)
139
+ self._cached_base64_image = base64.b64encode(img_bytes_resized.getvalue()).decode()
140
+ return self._cached_base64_image
62
141
 
63
- @cached_property
142
+ @property
64
143
  def mime_type(self):
65
- return Image.MIME[self._image.format]
144
+ return Image.MIME[self._image_format]
66
145
 
67
146
  @cached_property
68
147
  def data_url(self):
69
148
  return f"data:{self.mime_type};base64,{self.base64_image}"
70
-
@@ -1,36 +1,62 @@
1
1
  # @Author: Bi Ying
2
- # @Date: 2024-08-14 13:03:10
2
+ # @Date: 2024-06-07 16:16:49
3
3
  import time
4
+ from typing import Optional, Any, Callable, Tuple, Union, TypeVar, Generic
4
5
 
5
6
 
6
- class Retry:
7
- def __init__(self, function):
8
- self.function = function
9
- self.__retry_times = 3
10
- self.__sleep_time = 1
11
- self.pargs = []
12
- self.kwargs = {}
7
+ ResultType = TypeVar("ResultType")
13
8
 
14
- def args(self, *args, **kwargs):
15
- self.pargs = args
9
+
10
+ class Retry(Generic[ResultType]):
11
+ def __init__(self, function: Callable[..., ResultType]):
12
+ self.function: Callable[..., ResultType] = function
13
+ self.__retry_times: int = 3
14
+ self.__sleep_time: Union[int, float] = 1
15
+ self.__timeout: int = 180
16
+ self.__result_check: Optional[Callable[[ResultType], bool]] = None
17
+ self.pargs: list = []
18
+ self.kwargs: dict = {}
19
+
20
+ def args(self, *args: Any, **kwargs: Any) -> "Retry[ResultType]":
21
+ self.pargs = list(args)
16
22
  self.kwargs = kwargs
17
23
  return self
18
24
 
19
- def retry_times(self, retry_times: int):
25
+ def retry_times(self, retry_times: int) -> "Retry[ResultType]":
20
26
  self.__retry_times = retry_times
21
27
  return self
22
28
 
23
- def sleep_time(self, sleep_time):
29
+ def sleep_time(self, sleep_time: Union[int, float]) -> "Retry[ResultType]":
24
30
  self.__sleep_time = sleep_time
25
31
  return self
26
32
 
27
- def run(self):
33
+ def result_check(self, check_function: Callable[[ResultType], bool]) -> "Retry[ResultType]":
34
+ self.__result_check = check_function
35
+ return self
36
+
37
+ def _check_result(self, result: ResultType) -> bool:
38
+ try:
39
+ if self.__result_check is None:
40
+ return True
41
+ return self.__result_check(result)
42
+ except Exception as e:
43
+ print(f"Retry result check error: {e}")
44
+ return False
45
+
46
+ def run(self) -> Tuple[bool, Optional[ResultType]]:
28
47
  try_times = 0
29
- while try_times < self.__retry_times:
48
+ start_time = time.time()
49
+
50
+ while try_times <= self.__retry_times and time.time() - start_time < self.__timeout:
30
51
  try:
31
- return True, self.function(*self.pargs, **self.kwargs)
52
+ result: ResultType = self.function(*self.pargs, **self.kwargs)
53
+ if self._check_result(result):
54
+ return True, result
55
+ try_times += 1
56
+ time.sleep(self.__sleep_time)
32
57
  except Exception as e:
33
- print(f"{self.function.__name__} 函数出错:{e}")
58
+ print(f"{self.function.__name__} function error: {e}")
34
59
  try_times += 1
35
60
  time.sleep(self.__sleep_time)
61
+
36
62
  return False, None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vectorvein
3
- Version: 0.1.40
3
+ Version: 0.1.42
4
4
  Summary: Default template for PDM package
5
5
  Author-Email: Anderson <andersonby@163.com>
6
6
  License: MIT
@@ -13,6 +13,10 @@ Requires-Dist: pydantic>=2.8.2
13
13
  Requires-Dist: Pillow>=10.4.0
14
14
  Requires-Dist: deepseek-tokenizer>=0.1.0
15
15
  Requires-Dist: qwen-tokenizer>=0.2.0
16
+ Requires-Dist: google-auth>=2.35.0
17
+ Provides-Extra: server
18
+ Requires-Dist: fastapi; extra == "server"
19
+ Requires-Dist: uvicorn; extra == "server"
16
20
  Description-Content-Type: text/markdown
17
21
 
18
22
  # vectorvein
@@ -1,6 +1,6 @@
1
- vectorvein-0.1.40.dist-info/METADATA,sha256=Pb_5WD1aE0Y1CDJOPMvCFohJq0h_CcDd_tehqOpsEbk,502
2
- vectorvein-0.1.40.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
- vectorvein-0.1.40.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
1
+ vectorvein-0.1.42.dist-info/METADATA,sha256=nsKbuRtzNcT0Qkrt1q9DSNoMzlAg0PoJBcQ6570T9mU,644
2
+ vectorvein-0.1.42.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
+ vectorvein-0.1.42.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
4
  vectorvein/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  vectorvein/chat_clients/__init__.py,sha256=dW169oK1n3v8Z0uD8itghzlCP72rxiaS-XYn6fvI2xM,16788
6
6
  vectorvein/chat_clients/anthropic_client.py,sha256=jF9pDlnkhjM6-OLPCQQxkh27xjzbTRaEY53olRd3_aY,32413
@@ -18,17 +18,18 @@ vectorvein/chat_clients/openai_compatible_client.py,sha256=FVm_ZYL9UP6t6hTUNxPyo
18
18
  vectorvein/chat_clients/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  vectorvein/chat_clients/qwen_client.py,sha256=-ryh-m9PgsO0fc4ulcCmPTy1155J8YUy15uPoJQOHA0,513
20
20
  vectorvein/chat_clients/stepfun_client.py,sha256=zsD2W5ahmR4DD9cqQTXmJr3txrGuvxbRWhFlRdwNijI,519
21
- vectorvein/chat_clients/utils.py,sha256=7aSukIJrjFr50o160tgV7t2gUdZUdeNJnJaeJSwlBtQ,24383
21
+ vectorvein/chat_clients/utils.py,sha256=CRwjzMkYRHs0onKf87KxseG08b4zjAjZxi00-gQPYHc,25253
22
22
  vectorvein/chat_clients/yi_client.py,sha256=RNf4CRuPJfixrwLZ3-DEc3t25QDe1mvZeb9sku2f8Bc,484
23
23
  vectorvein/chat_clients/zhipuai_client.py,sha256=Ys5DSeLCuedaDXr3PfG1EW2zKXopt-awO2IylWSwY0s,519
24
24
  vectorvein/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- vectorvein/settings/__init__.py,sha256=0L-2WicBq9ctaJRoSwx8ZhVtX4slS5tHrIlSGf-tJxg,3564
25
+ vectorvein/server/token_server.py,sha256=mk7hbhagBc3q-5a7q3o0wNBkx-f0A4GTShqDD2bwkqA,1367
26
+ vectorvein/settings/__init__.py,sha256=tYC8hSAYIUxpcJTx7zhpTSdNqc5fVeYPE1IhOTTTpRA,3684
26
27
  vectorvein/settings/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
28
  vectorvein/types/defaults.py,sha256=xefmRNYBGbnWA5kjLLFKN91UM5gnHZ5-kcCNlQRfznk,22095
28
29
  vectorvein/types/enums.py,sha256=x_S0IJiEWijOAEiMNdiGDGEWGtmt7TwMriJVDqrDmTo,1637
29
30
  vectorvein/types/exception.py,sha256=gnW4GnJ76jND6UGnodk9xmqkcbeS7Cz2rvncA2HpD5E,69
30
31
  vectorvein/types/llm_parameters.py,sha256=vhleSgCHzDl7EULYJ3dUYlu9KLbfs9y6dcPD0BkaRdg,5114
31
32
  vectorvein/types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- vectorvein/utilities/media_processing.py,sha256=BujciRmw1GMmc3ELRvafL8STcy6r5b2rVnh27-uA7so,2256
33
- vectorvein/utilities/retry.py,sha256=9ePuJdeUUGx-qMWfaFxmlOvG_lQPwCQ4UB1z3Edlo34,993
34
- vectorvein-0.1.40.dist-info/RECORD,,
33
+ vectorvein/utilities/media_processing.py,sha256=cnzLrU1OaJvSv87IOnc36FrDXtmGMDStPbxtIJ33YN4,5880
34
+ vectorvein/utilities/retry.py,sha256=6KFS9R2HdhqM3_9jkjD4F36ZSpEx2YNFGOVlpOsUetM,2208
35
+ vectorvein-0.1.42.dist-info/RECORD,,