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.
Files changed (39) hide show
  1. flexllm/__init__.py +224 -0
  2. flexllm/__main__.py +1096 -0
  3. flexllm/async_api/__init__.py +9 -0
  4. flexllm/async_api/concurrent_call.py +100 -0
  5. flexllm/async_api/concurrent_executor.py +1036 -0
  6. flexllm/async_api/core.py +373 -0
  7. flexllm/async_api/interface.py +12 -0
  8. flexllm/async_api/progress.py +277 -0
  9. flexllm/base_client.py +988 -0
  10. flexllm/batch_tools/__init__.py +16 -0
  11. flexllm/batch_tools/folder_processor.py +317 -0
  12. flexllm/batch_tools/table_processor.py +363 -0
  13. flexllm/cache/__init__.py +10 -0
  14. flexllm/cache/response_cache.py +293 -0
  15. flexllm/chain_of_thought_client.py +1120 -0
  16. flexllm/claudeclient.py +402 -0
  17. flexllm/client_pool.py +698 -0
  18. flexllm/geminiclient.py +563 -0
  19. flexllm/llm_client.py +523 -0
  20. flexllm/llm_parser.py +60 -0
  21. flexllm/mllm_client.py +559 -0
  22. flexllm/msg_processors/__init__.py +174 -0
  23. flexllm/msg_processors/image_processor.py +729 -0
  24. flexllm/msg_processors/image_processor_helper.py +485 -0
  25. flexllm/msg_processors/messages_processor.py +341 -0
  26. flexllm/msg_processors/unified_processor.py +1404 -0
  27. flexllm/openaiclient.py +256 -0
  28. flexllm/pricing/__init__.py +104 -0
  29. flexllm/pricing/data.json +1201 -0
  30. flexllm/pricing/updater.py +223 -0
  31. flexllm/provider_router.py +213 -0
  32. flexllm/token_counter.py +270 -0
  33. flexllm/utils/__init__.py +1 -0
  34. flexllm/utils/core.py +41 -0
  35. flexllm-0.3.3.dist-info/METADATA +573 -0
  36. flexllm-0.3.3.dist-info/RECORD +39 -0
  37. flexllm-0.3.3.dist-info/WHEEL +4 -0
  38. flexllm-0.3.3.dist-info/entry_points.txt +3 -0
  39. 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