flexllm 0.3.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.
- flexllm/__init__.py +224 -0
- flexllm/__main__.py +1096 -0
- flexllm/async_api/__init__.py +9 -0
- flexllm/async_api/concurrent_call.py +100 -0
- flexllm/async_api/concurrent_executor.py +1036 -0
- flexllm/async_api/core.py +373 -0
- flexllm/async_api/interface.py +12 -0
- flexllm/async_api/progress.py +277 -0
- flexllm/base_client.py +988 -0
- flexllm/batch_tools/__init__.py +16 -0
- flexllm/batch_tools/folder_processor.py +317 -0
- flexllm/batch_tools/table_processor.py +363 -0
- flexllm/cache/__init__.py +10 -0
- flexllm/cache/response_cache.py +293 -0
- flexllm/chain_of_thought_client.py +1120 -0
- flexllm/claudeclient.py +402 -0
- flexllm/client_pool.py +698 -0
- flexllm/geminiclient.py +563 -0
- flexllm/llm_client.py +523 -0
- flexllm/llm_parser.py +60 -0
- flexllm/mllm_client.py +559 -0
- flexllm/msg_processors/__init__.py +174 -0
- flexllm/msg_processors/image_processor.py +729 -0
- flexllm/msg_processors/image_processor_helper.py +485 -0
- flexllm/msg_processors/messages_processor.py +341 -0
- flexllm/msg_processors/unified_processor.py +1404 -0
- flexllm/openaiclient.py +256 -0
- flexllm/pricing/__init__.py +104 -0
- flexllm/pricing/data.json +1201 -0
- flexllm/pricing/updater.py +223 -0
- flexllm/provider_router.py +213 -0
- flexllm/token_counter.py +270 -0
- flexllm/utils/__init__.py +1 -0
- flexllm/utils/core.py +41 -0
- flexllm-0.3.3.dist-info/METADATA +573 -0
- flexllm-0.3.3.dist-info/RECORD +39 -0
- flexllm-0.3.3.dist-info/WHEEL +4 -0
- flexllm-0.3.3.dist-info/entry_points.txt +3 -0
- flexllm-0.3.3.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
import aiohttp
|
|
4
|
+
import time
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from typing import Optional, Callable, TYPE_CHECKING
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .image_processor import ImageCacheConfig
|
|
12
|
+
else:
|
|
13
|
+
ImageCacheConfig = None # 避免mypy等类型检查器报错
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from tqdm.asyncio import tqdm
|
|
17
|
+
|
|
18
|
+
TQDM_AVAILABLE = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
TQDM_AVAILABLE = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def process_content_recursive(
|
|
24
|
+
content, session, cache_config: Optional["ImageCacheConfig"] = None, **kwargs
|
|
25
|
+
):
|
|
26
|
+
"""Recursively process a content dictionary, replacing any URL with its Base64 equivalent.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
content: Content dictionary to process
|
|
30
|
+
session: aiohttp.ClientSession for async URL fetching
|
|
31
|
+
cache_config: Image cache configuration, if None or disabled, no caching will be used
|
|
32
|
+
**kwargs: Additional arguments to pass to image processing functions
|
|
33
|
+
"""
|
|
34
|
+
from .image_processor import encode_image_to_base64
|
|
35
|
+
|
|
36
|
+
if isinstance(content, dict):
|
|
37
|
+
for key, value in content.items():
|
|
38
|
+
if key == "url" and isinstance(value, str): # Detect URL fields
|
|
39
|
+
base64_data = await encode_image_to_base64(
|
|
40
|
+
value,
|
|
41
|
+
session,
|
|
42
|
+
max_width=kwargs.get("max_width"),
|
|
43
|
+
max_height=kwargs.get("max_height"),
|
|
44
|
+
max_pixels=kwargs.get("max_pixels"),
|
|
45
|
+
cache_config=cache_config,
|
|
46
|
+
)
|
|
47
|
+
if base64_data:
|
|
48
|
+
content[key] = base64_data
|
|
49
|
+
else:
|
|
50
|
+
await process_content_recursive(
|
|
51
|
+
value, session, cache_config=cache_config, **kwargs
|
|
52
|
+
)
|
|
53
|
+
elif isinstance(content, list):
|
|
54
|
+
for item in content:
|
|
55
|
+
await process_content_recursive(
|
|
56
|
+
item, session, cache_config=cache_config, **kwargs
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def messages_preprocess(
|
|
61
|
+
messages, inplace=False, cache_config: Optional["ImageCacheConfig"] = None, **kwargs
|
|
62
|
+
):
|
|
63
|
+
"""Process a list of messages, converting URLs in any type of content to Base64.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
messages: List of messages to process
|
|
67
|
+
inplace: Whether to modify the messages in-place or create a copy
|
|
68
|
+
cache_config: Image cache configuration object
|
|
69
|
+
**kwargs: Additional arguments to pass to image processing functions
|
|
70
|
+
"""
|
|
71
|
+
if not inplace:
|
|
72
|
+
messages = deepcopy(messages)
|
|
73
|
+
|
|
74
|
+
async with aiohttp.ClientSession() as session:
|
|
75
|
+
tasks = [
|
|
76
|
+
process_content_recursive(
|
|
77
|
+
message, session, cache_config=cache_config, **kwargs
|
|
78
|
+
)
|
|
79
|
+
for message in messages
|
|
80
|
+
]
|
|
81
|
+
await asyncio.gather(*tasks)
|
|
82
|
+
return messages
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def batch_messages_preprocess(
|
|
86
|
+
messages_list,
|
|
87
|
+
max_concurrent=5,
|
|
88
|
+
inplace=False,
|
|
89
|
+
cache_config: Optional["ImageCacheConfig"] = None,
|
|
90
|
+
as_iterator=False,
|
|
91
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
92
|
+
show_progress: bool = False,
|
|
93
|
+
progress_desc: str = "处理消息",
|
|
94
|
+
**kwargs,
|
|
95
|
+
):
|
|
96
|
+
"""Process multiple lists of messages in batches.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
messages_list: List, iterator or async iterator of message lists to process
|
|
100
|
+
max_concurrent: Maximum number of concurrent batches to process
|
|
101
|
+
inplace: Whether to modify the messages in-place
|
|
102
|
+
cache_config: Image cache configuration object
|
|
103
|
+
as_iterator: Whether to return an async iterator instead of a list
|
|
104
|
+
progress_callback: Optional callback function to report progress (current, total)
|
|
105
|
+
show_progress: Whether to show a progress bar using tqdm
|
|
106
|
+
progress_desc: Description for the progress bar
|
|
107
|
+
**kwargs: Additional arguments to pass to image processing functions
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of processed message lists or an async iterator yielding processed message lists
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
# 创建处理单个消息列表的函数
|
|
114
|
+
async def process_single_batch(messages, semaphore, index=None):
|
|
115
|
+
async with semaphore:
|
|
116
|
+
try:
|
|
117
|
+
processed_messages = await messages_preprocess(
|
|
118
|
+
messages, inplace=inplace, cache_config=cache_config, **kwargs
|
|
119
|
+
)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"{e=}\n")
|
|
122
|
+
processed_messages = messages
|
|
123
|
+
return processed_messages, index
|
|
124
|
+
|
|
125
|
+
# 进度报告函数
|
|
126
|
+
def report_progress(current: int, total: int, start_time: float = None):
|
|
127
|
+
if progress_callback:
|
|
128
|
+
try:
|
|
129
|
+
# 计算时间信息
|
|
130
|
+
elapsed_time = time.time() - start_time if start_time else 0
|
|
131
|
+
|
|
132
|
+
# 创建扩展的进度信息
|
|
133
|
+
progress_info = {
|
|
134
|
+
"current": current,
|
|
135
|
+
"total": total,
|
|
136
|
+
"percentage": (current / total * 100) if total > 0 else 0,
|
|
137
|
+
"elapsed_time": elapsed_time,
|
|
138
|
+
"estimated_total_time": (elapsed_time / current * total)
|
|
139
|
+
if current > 0
|
|
140
|
+
else 0,
|
|
141
|
+
"estimated_remaining_time": (
|
|
142
|
+
elapsed_time / current * (total - current)
|
|
143
|
+
)
|
|
144
|
+
if current > 0
|
|
145
|
+
else 0,
|
|
146
|
+
"rate": current / elapsed_time if elapsed_time > 0 else 0,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# 如果回调函数接受单个参数,传递扩展信息;否则保持兼容性
|
|
150
|
+
import inspect
|
|
151
|
+
|
|
152
|
+
sig = inspect.signature(progress_callback)
|
|
153
|
+
if len(sig.parameters) == 1:
|
|
154
|
+
progress_callback(progress_info)
|
|
155
|
+
else:
|
|
156
|
+
progress_callback(current, total)
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.warning(f"进度回调函数执行失败: {e}")
|
|
160
|
+
|
|
161
|
+
# 如果要求返回迭代器
|
|
162
|
+
if as_iterator:
|
|
163
|
+
|
|
164
|
+
async def process_iterator():
|
|
165
|
+
semaphore = asyncio.Semaphore(max_concurrent)
|
|
166
|
+
|
|
167
|
+
# 检查是否为异步迭代器
|
|
168
|
+
is_async_iterator = hasattr(messages_list, "__aiter__")
|
|
169
|
+
|
|
170
|
+
processed_count = 0
|
|
171
|
+
total_count = None
|
|
172
|
+
messages_to_process = messages_list # 使用新变量名避免作用域问题
|
|
173
|
+
|
|
174
|
+
# 如果可以获取总数,先计算总数
|
|
175
|
+
if not is_async_iterator and hasattr(messages_list, "__len__"):
|
|
176
|
+
total_count = len(messages_list)
|
|
177
|
+
elif not is_async_iterator:
|
|
178
|
+
# 对于迭代器,先转换为列表获取长度
|
|
179
|
+
messages_list_converted = list(messages_list)
|
|
180
|
+
total_count = len(messages_list_converted)
|
|
181
|
+
messages_to_process = iter(messages_list_converted) # 使用新变量名
|
|
182
|
+
|
|
183
|
+
# 创建进度条
|
|
184
|
+
pbar = None
|
|
185
|
+
start_time = time.time()
|
|
186
|
+
if show_progress and TQDM_AVAILABLE and total_count:
|
|
187
|
+
# 自定义进度条格式,显示时间信息
|
|
188
|
+
bar_format = "{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]"
|
|
189
|
+
pbar = tqdm(
|
|
190
|
+
total=total_count,
|
|
191
|
+
desc=progress_desc,
|
|
192
|
+
unit="批次",
|
|
193
|
+
bar_format=bar_format,
|
|
194
|
+
ncols=100, # 控制进度条宽度
|
|
195
|
+
miniters=1, # 每次更新都显示
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
# 处理异步迭代器
|
|
200
|
+
if is_async_iterator:
|
|
201
|
+
pending_tasks = []
|
|
202
|
+
task_index = 0
|
|
203
|
+
async for messages in messages_to_process:
|
|
204
|
+
# 如果已经达到最大并发数,等待一个任务完成
|
|
205
|
+
if len(pending_tasks) >= max_concurrent:
|
|
206
|
+
done, pending_tasks = await asyncio.wait(
|
|
207
|
+
pending_tasks, return_when=asyncio.FIRST_COMPLETED
|
|
208
|
+
)
|
|
209
|
+
for task in done:
|
|
210
|
+
result, _ = await task
|
|
211
|
+
processed_count += 1
|
|
212
|
+
if pbar:
|
|
213
|
+
pbar.update(1)
|
|
214
|
+
report_progress(
|
|
215
|
+
processed_count,
|
|
216
|
+
total_count or processed_count,
|
|
217
|
+
start_time,
|
|
218
|
+
)
|
|
219
|
+
yield result
|
|
220
|
+
|
|
221
|
+
# 创建新任务
|
|
222
|
+
task = asyncio.create_task(
|
|
223
|
+
process_single_batch(messages, semaphore, task_index)
|
|
224
|
+
)
|
|
225
|
+
pending_tasks.append(task)
|
|
226
|
+
task_index += 1
|
|
227
|
+
|
|
228
|
+
# 等待所有剩余任务完成
|
|
229
|
+
if pending_tasks:
|
|
230
|
+
for task in asyncio.as_completed(pending_tasks):
|
|
231
|
+
result, _ = await task
|
|
232
|
+
processed_count += 1
|
|
233
|
+
if pbar:
|
|
234
|
+
pbar.update(1)
|
|
235
|
+
report_progress(
|
|
236
|
+
processed_count,
|
|
237
|
+
total_count or processed_count,
|
|
238
|
+
start_time,
|
|
239
|
+
)
|
|
240
|
+
yield result
|
|
241
|
+
|
|
242
|
+
# 处理同步迭代器或列表
|
|
243
|
+
else:
|
|
244
|
+
# 转换为列表以避免消耗迭代器
|
|
245
|
+
if not isinstance(messages_to_process, (list, tuple)):
|
|
246
|
+
messages_list_converted = list(messages_to_process)
|
|
247
|
+
else:
|
|
248
|
+
messages_list_converted = messages_to_process
|
|
249
|
+
|
|
250
|
+
if not total_count:
|
|
251
|
+
total_count = len(messages_list_converted)
|
|
252
|
+
if pbar:
|
|
253
|
+
pbar.total = total_count
|
|
254
|
+
|
|
255
|
+
# 分批处理
|
|
256
|
+
for i in range(0, len(messages_list_converted), max_concurrent):
|
|
257
|
+
batch = messages_list_converted[i : i + max_concurrent]
|
|
258
|
+
tasks = [
|
|
259
|
+
process_single_batch(messages, semaphore, i + j)
|
|
260
|
+
for j, messages in enumerate(batch)
|
|
261
|
+
]
|
|
262
|
+
results = await asyncio.gather(*tasks)
|
|
263
|
+
|
|
264
|
+
for result, _ in results:
|
|
265
|
+
processed_count += 1
|
|
266
|
+
if pbar:
|
|
267
|
+
pbar.update(1)
|
|
268
|
+
report_progress(processed_count, total_count, start_time)
|
|
269
|
+
yield result
|
|
270
|
+
finally:
|
|
271
|
+
if pbar:
|
|
272
|
+
pbar.close()
|
|
273
|
+
|
|
274
|
+
return process_iterator()
|
|
275
|
+
|
|
276
|
+
# 原始实现,返回列表
|
|
277
|
+
else:
|
|
278
|
+
semaphore = asyncio.Semaphore(max_concurrent)
|
|
279
|
+
|
|
280
|
+
# 检查是否为异步迭代器
|
|
281
|
+
is_async_iterator = hasattr(messages_list, "__aiter__")
|
|
282
|
+
|
|
283
|
+
# 转换为列表
|
|
284
|
+
if is_async_iterator:
|
|
285
|
+
messages_list_converted = []
|
|
286
|
+
async for messages in messages_list:
|
|
287
|
+
messages_list_converted.append(messages)
|
|
288
|
+
elif not isinstance(messages_list, (list, tuple)):
|
|
289
|
+
messages_list_converted = list(messages_list)
|
|
290
|
+
else:
|
|
291
|
+
messages_list_converted = messages_list
|
|
292
|
+
|
|
293
|
+
if not messages_list_converted:
|
|
294
|
+
return []
|
|
295
|
+
|
|
296
|
+
total_count = len(messages_list_converted)
|
|
297
|
+
processed_count = 0
|
|
298
|
+
|
|
299
|
+
# 创建进度条
|
|
300
|
+
pbar = None
|
|
301
|
+
start_time = time.time()
|
|
302
|
+
if show_progress and TQDM_AVAILABLE:
|
|
303
|
+
# 自定义进度条格式,显示时间信息
|
|
304
|
+
bar_format = (
|
|
305
|
+
"{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]"
|
|
306
|
+
)
|
|
307
|
+
pbar = tqdm(
|
|
308
|
+
total=total_count,
|
|
309
|
+
desc=progress_desc,
|
|
310
|
+
unit=" items",
|
|
311
|
+
bar_format=bar_format,
|
|
312
|
+
ncols=100, # 控制进度条宽度
|
|
313
|
+
miniters=1, # 每次更新都显示
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
# 分批处理以实现进度更新
|
|
318
|
+
results = []
|
|
319
|
+
for i in range(0, len(messages_list_converted), max_concurrent):
|
|
320
|
+
batch = messages_list_converted[i : i + max_concurrent]
|
|
321
|
+
tasks = [
|
|
322
|
+
process_single_batch(messages, semaphore, i + j)
|
|
323
|
+
for j, messages in enumerate(batch)
|
|
324
|
+
]
|
|
325
|
+
batch_results = await asyncio.gather(*tasks)
|
|
326
|
+
|
|
327
|
+
for result, _ in batch_results:
|
|
328
|
+
results.append(result)
|
|
329
|
+
processed_count += 1
|
|
330
|
+
if pbar:
|
|
331
|
+
pbar.update(1)
|
|
332
|
+
report_progress(processed_count, total_count, start_time)
|
|
333
|
+
|
|
334
|
+
return results
|
|
335
|
+
finally:
|
|
336
|
+
if pbar:
|
|
337
|
+
pbar.close()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# 为了向后兼容,提供别名
|
|
341
|
+
batch_process_messages = batch_messages_preprocess
|