infinity_llm_client 0.1.0__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.
- infinity_llm_client-0.1.0.dist-info/METADATA +110 -0
- infinity_llm_client-0.1.0.dist-info/RECORD +26 -0
- infinity_llm_client-0.1.0.dist-info/WHEEL +5 -0
- infinity_llm_client-0.1.0.dist-info/licenses/LICENSE.md +21 -0
- infinity_llm_client-0.1.0.dist-info/top_level.txt +1 -0
- llm/__init__.py +60 -0
- llm/clients/__init__.py +63 -0
- llm/clients/base.py +78 -0
- llm/clients/config.py +28 -0
- llm/clients/exceptions.py +129 -0
- llm/clients/open_ai/__init__.py +27 -0
- llm/clients/open_ai/aggregation.py +63 -0
- llm/clients/open_ai/client.py +261 -0
- llm/clients/open_ai/config.py +34 -0
- llm/clients/open_ai/connection.py +167 -0
- llm/clients/open_ai/open_ai_models.py +121 -0
- llm/clients/open_ai/response_format.py +42 -0
- llm/clients/provider.py +38 -0
- llm/models/__init__.py +58 -0
- llm/models/chunks.py +47 -0
- llm/models/messages.py +131 -0
- llm/models/tools.py +213 -0
- llm/py.typed +0 -0
- llm/tools/__init__.py +6 -0
- llm/tools/introspection.py +378 -0
- llm/tools/registry.py +251 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: infinity_llm_client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 强类型、可扩展的 LLM 异步客户端库,支持 OpenAI 兼容 API、流式对话、工具调用与多模型消息格式
|
|
5
|
+
Author: infinity_system: yin_bailiang
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/yinbailiang/infinity_llm
|
|
8
|
+
Keywords: llm,openai,async,asyncio,function-calling,tool-calling,streaming,chat
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Framework :: AsyncIO
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE.md
|
|
21
|
+
Requires-Dist: pydantic
|
|
22
|
+
Requires-Dist: aiohttp
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest; extra == "test"
|
|
25
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: interrogate; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff; extra == "dev"
|
|
30
|
+
Requires-Dist: pyright; extra == "dev"
|
|
31
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
32
|
+
Requires-Dist: infinity_llm_client[test]; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# InfinityLLM — Async LLM Client
|
|
36
|
+
|
|
37
|
+
[](https://github.com/yinbailiang/infinity_llm/actions/workflows/test.yml)
|
|
38
|
+
[](ENGINEERING.md)
|
|
39
|
+
[](LICENSE.md)
|
|
40
|
+
[](https://pypi.org/project/infinity_llm/)
|
|
41
|
+
|
|
42
|
+
**强类型、可扩展的 LLM 异步客户端库 — 支持 OpenAI 兼容 API、流式对话、工具调用。**
|
|
43
|
+
|
|
44
|
+
## ✨ Features
|
|
45
|
+
|
|
46
|
+
| 类别 | 能力 |
|
|
47
|
+
| - | - |
|
|
48
|
+
| 类型安全 | Pydantic 负载校验 · pyright **strict** |
|
|
49
|
+
| 多 Provider | 工厂注册模式,轻松扩展新 LLM 服务商 |
|
|
50
|
+
| OpenAI 兼容 | 完整实现 OpenAI Chat Completions 流式 API |
|
|
51
|
+
| 工具调用 | ToolRegistry 自动从 Python 函数推断 Tool Schema |
|
|
52
|
+
| 流式对话 | `stream_chat()` 异步生成器,支持 Text / ToolCall / Usage chunk |
|
|
53
|
+
| 多模态消息 | 文本 + 图片 URL 混合内容 |
|
|
54
|
+
| 连接管理 | 指数退避重试 + 抖动 + 连接池复用 |
|
|
55
|
+
|
|
56
|
+
## 📦 Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install infinity_llm
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 🚀 Quick Start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from llm import (
|
|
66
|
+
Message, Messages,
|
|
67
|
+
OpenAIConfig, create_client,
|
|
68
|
+
ToolRegistry,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# 创建客户端
|
|
72
|
+
config = OpenAIConfig(
|
|
73
|
+
model="gpt-4o-mini",
|
|
74
|
+
api_key="sk-...",
|
|
75
|
+
)
|
|
76
|
+
client = create_client(config)
|
|
77
|
+
|
|
78
|
+
# 流式对话
|
|
79
|
+
messages = Messages([
|
|
80
|
+
Message.system("你是一个有用的助手"),
|
|
81
|
+
Message.user("你好"),
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
async with client:
|
|
85
|
+
async for chunk in client.stream_chat(messages):
|
|
86
|
+
print(chunk)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 🧱 Architecture
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
llm/
|
|
93
|
+
├── clients/ # 客户端抽象 + provider 实现
|
|
94
|
+
│ ├── base.py # LLMClient 抽象基类
|
|
95
|
+
│ ├── config.py # ConnectionConfig / LLMConfig
|
|
96
|
+
│ ├── exceptions.py # 异常层级
|
|
97
|
+
│ ├── provider.py # 工厂注册表
|
|
98
|
+
│ └── open_ai/ # OpenAI 兼容实现
|
|
99
|
+
├── models/ # 消息 / Chunk / 工具定义
|
|
100
|
+
│ ├── messages.py # Message, Messages, ToolCall
|
|
101
|
+
│ ├── chunks.py # StreamChunk 子类
|
|
102
|
+
│ └── tools.py # ToolDefinition, ParameterProperty
|
|
103
|
+
└── tools/ # 工具注册与类型推断
|
|
104
|
+
├── registry.py # ToolRegistry
|
|
105
|
+
└── introspection.py # 函数 → ToolDefinition 自动推断
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 📄 License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
infinity_llm_client-0.1.0.dist-info/licenses/LICENSE.md,sha256=MIiitE-EgxYub_R0ZQO4inTK5vTV8DqV-2NNuT29axE,1074
|
|
2
|
+
llm/__init__.py,sha256=hEi-pnBtjNyLWiiZ_9QtMBTHOGxjcgBmUb9mqMK2GXo,1065
|
|
3
|
+
llm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
llm/clients/__init__.py,sha256=aJ0P2ArkKhBM_rl0r-drmIaeRSxoyYFIRh0tzwKWHdI,1356
|
|
5
|
+
llm/clients/base.py,sha256=xqNhzoRVKPWhwEWeHgBgRFjFirKR1GyYF7ah0n-nUI4,2125
|
|
6
|
+
llm/clients/config.py,sha256=NjodJ7s15tz1MVEOjg3yCEb2mMpZD23Pr3Aj6GmWJpA,1281
|
|
7
|
+
llm/clients/exceptions.py,sha256=BwCrWqlJDP1NllLk4O5gZQZVTTOT2QEGp7Vpioe5T98,2852
|
|
8
|
+
llm/clients/provider.py,sha256=Amr_KTlNVoJyENAv7NYxO6HH1JWGBIsis8w43dyI1F0,1066
|
|
9
|
+
llm/clients/open_ai/__init__.py,sha256=9guvJ2sMe39_XD_gqJg0kIbqlEKzC-eY3-PUl9E-2Qw,622
|
|
10
|
+
llm/clients/open_ai/aggregation.py,sha256=1Nm7TDL4esbtrY6dYm5_8MQH9wlX9pygMnO9Q42x9Wk,2096
|
|
11
|
+
llm/clients/open_ai/client.py,sha256=qPmm1qzNd37Tqt2AWyU9jZlmK7wGMXrgGxK3BcrvWN0,8422
|
|
12
|
+
llm/clients/open_ai/config.py,sha256=nRczIlCsjKn5VYlmkCC-gY7VCrkTPl5kC3TsxnZnc_Q,1050
|
|
13
|
+
llm/clients/open_ai/connection.py,sha256=21jCIDmapwQlhqcmEbjjku_B7NWaVrrDWwLuc-TF-lA,5944
|
|
14
|
+
llm/clients/open_ai/open_ai_models.py,sha256=-P9nR0t6uAvtbewruPdSX4RBP5km-WLc2fhBvbcXohU,5352
|
|
15
|
+
llm/clients/open_ai/response_format.py,sha256=kaGkcn4BRiBULBJ8Y3ZtyxXZp6VIBtftcnkc6HI7Nj0,1333
|
|
16
|
+
llm/models/__init__.py,sha256=XOilrS5S02VNIjhf1_YtQ-tpNmn5FG4aJXdiE-SbhT0,1031
|
|
17
|
+
llm/models/chunks.py,sha256=3srBgqfaqKZ4MUAT8zccBDNtC8f2Zc8nNWxkTaC3SRY,1085
|
|
18
|
+
llm/models/messages.py,sha256=masAosZcpcTk2wXobaQjpCq0qmq-d7VAYOKjbh-kkrE,5059
|
|
19
|
+
llm/models/tools.py,sha256=Yh38k0aImLgbuYBbxKr5sF_CchpjfI3Bm2bC-nfJDhk,8689
|
|
20
|
+
llm/tools/__init__.py,sha256=Ogp3oJ6_rI5jTqjXKL2CxdTxGBDfkbdteKmbQzn6fqg,162
|
|
21
|
+
llm/tools/introspection.py,sha256=XkVmWA1tXB7PtXtqwdCLzgwpQ17FhyCZgWGkLxkiQvc,14098
|
|
22
|
+
llm/tools/registry.py,sha256=K8dcU2EP53WYY9v22e64XhsTvvV-3RnZc8evakOb2ok,9267
|
|
23
|
+
infinity_llm_client-0.1.0.dist-info/METADATA,sha256=MlaLt91TBvlSomxVKLZ1aZBGQCSZNa2ZwYmw1OarnH4,3839
|
|
24
|
+
infinity_llm_client-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
25
|
+
infinity_llm_client-0.1.0.dist-info/top_level.txt,sha256=TwpEQXP3b1MS9Y2XuGgRuo9-Kny507xt2HFZgJ5TSIY,4
|
|
26
|
+
infinity_llm_client-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 infinity_system
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
llm
|
llm/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""LLM 模块公共 API"""
|
|
2
|
+
|
|
3
|
+
__version__ = '0.1.0'
|
|
4
|
+
|
|
5
|
+
from .models.chunks import (
|
|
6
|
+
DoneChunk,
|
|
7
|
+
FinishChunk,
|
|
8
|
+
StreamChunk,
|
|
9
|
+
TextChunk,
|
|
10
|
+
ToolCallCompleteChunk,
|
|
11
|
+
UsageChunk,
|
|
12
|
+
UsageStats,
|
|
13
|
+
)
|
|
14
|
+
from .models.messages import (
|
|
15
|
+
ContentType,
|
|
16
|
+
FunctionCall,
|
|
17
|
+
ImageUrl,
|
|
18
|
+
Message,
|
|
19
|
+
MessageContent,
|
|
20
|
+
MessageRole,
|
|
21
|
+
Messages,
|
|
22
|
+
MultiModalContent,
|
|
23
|
+
ToolCall,
|
|
24
|
+
)
|
|
25
|
+
from .models.tools import (
|
|
26
|
+
ParameterProperty,
|
|
27
|
+
ToolDefinition,
|
|
28
|
+
ToolDefinitions,
|
|
29
|
+
ToolFunction,
|
|
30
|
+
ToolParameters,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
'__version__',
|
|
35
|
+
# -- 消息 --
|
|
36
|
+
'Message',
|
|
37
|
+
'MessageRole',
|
|
38
|
+
'Messages',
|
|
39
|
+
'MessageContent',
|
|
40
|
+
'ContentType',
|
|
41
|
+
'MultiModalContent',
|
|
42
|
+
'ImageUrl',
|
|
43
|
+
# -- 工具调用 --
|
|
44
|
+
'ToolCall',
|
|
45
|
+
'FunctionCall',
|
|
46
|
+
# -- 工具定义 --
|
|
47
|
+
'ToolDefinition',
|
|
48
|
+
'ToolDefinitions',
|
|
49
|
+
'ToolFunction',
|
|
50
|
+
'ToolParameters',
|
|
51
|
+
'ParameterProperty',
|
|
52
|
+
# -- 流式 Chunk --
|
|
53
|
+
'StreamChunk',
|
|
54
|
+
'TextChunk',
|
|
55
|
+
'ToolCallCompleteChunk',
|
|
56
|
+
'UsageChunk',
|
|
57
|
+
'FinishChunk',
|
|
58
|
+
'DoneChunk',
|
|
59
|
+
'UsageStats',
|
|
60
|
+
]
|
llm/clients/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from ..models import (
|
|
2
|
+
DoneChunk,
|
|
3
|
+
FinishChunk,
|
|
4
|
+
StreamChunk,
|
|
5
|
+
TextChunk,
|
|
6
|
+
ToolCallCompleteChunk,
|
|
7
|
+
UsageChunk,
|
|
8
|
+
UsageStats,
|
|
9
|
+
)
|
|
10
|
+
from .base import LLMClient
|
|
11
|
+
from .config import ConnectionConfig, LLMConfig
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
LLMAuthError,
|
|
14
|
+
LLMConfigError,
|
|
15
|
+
LLMContentFilterError,
|
|
16
|
+
LLMContextLengthError,
|
|
17
|
+
LLMError,
|
|
18
|
+
LLMHTTPError,
|
|
19
|
+
LLMInsufficientBalanceError,
|
|
20
|
+
LLMNetworkError,
|
|
21
|
+
LLMRateLimitError,
|
|
22
|
+
LLMRequestError,
|
|
23
|
+
LLMServerError,
|
|
24
|
+
LLMStreamError,
|
|
25
|
+
build_http_error,
|
|
26
|
+
)
|
|
27
|
+
from .open_ai import OpenAIConfig
|
|
28
|
+
from .provider import create_client, register_client
|
|
29
|
+
|
|
30
|
+
__all__: list[str] = [
|
|
31
|
+
# -- 客户端 --
|
|
32
|
+
'LLMClient',
|
|
33
|
+
# -- 配置 --
|
|
34
|
+
'ConnectionConfig',
|
|
35
|
+
'LLMConfig',
|
|
36
|
+
'create_client',
|
|
37
|
+
'register_client',
|
|
38
|
+
# -- provider 配置 --
|
|
39
|
+
'OpenAIConfig',
|
|
40
|
+
# -- 流式 Chunk --
|
|
41
|
+
'StreamChunk',
|
|
42
|
+
'TextChunk',
|
|
43
|
+
'ToolCallCompleteChunk',
|
|
44
|
+
'UsageChunk',
|
|
45
|
+
'FinishChunk',
|
|
46
|
+
'DoneChunk',
|
|
47
|
+
'UsageStats',
|
|
48
|
+
# -- 异常 --
|
|
49
|
+
'LLMError',
|
|
50
|
+
'LLMConfigError',
|
|
51
|
+
'LLMRequestError',
|
|
52
|
+
'LLMNetworkError',
|
|
53
|
+
'LLMHTTPError',
|
|
54
|
+
'LLMAuthError',
|
|
55
|
+
'LLMInsufficientBalanceError',
|
|
56
|
+
'LLMRateLimitError',
|
|
57
|
+
'LLMServerError',
|
|
58
|
+
'LLMContentFilterError',
|
|
59
|
+
'LLMContextLengthError',
|
|
60
|
+
'LLMStreamError',
|
|
61
|
+
# -- 工具函数 --
|
|
62
|
+
'build_http_error',
|
|
63
|
+
]
|
llm/clients/base.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""LLM 客户端抽象基类"""
|
|
2
|
+
|
|
3
|
+
import types
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import AsyncGenerator, Optional
|
|
6
|
+
|
|
7
|
+
from ..models import (
|
|
8
|
+
Message,
|
|
9
|
+
Messages,
|
|
10
|
+
StreamChunk,
|
|
11
|
+
TextChunk,
|
|
12
|
+
ToolCall,
|
|
13
|
+
ToolCallCompleteChunk,
|
|
14
|
+
ToolDefinitions,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LLMClient(ABC):
|
|
19
|
+
"""LLM 客户端抽象基类"""
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
async def __aenter__(self) -> 'LLMClient':
|
|
23
|
+
"""支持 async with 资源管理"""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
async def __aexit__(
|
|
28
|
+
self,
|
|
29
|
+
exc_type: Optional[type[BaseException]],
|
|
30
|
+
exc_val: Optional[BaseException],
|
|
31
|
+
exc_tb: Optional[types.TracebackType],
|
|
32
|
+
) -> Optional[bool]: ...
|
|
33
|
+
|
|
34
|
+
# ---------- 核心对话接口 ----------
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def stream_chat(
|
|
38
|
+
self,
|
|
39
|
+
messages: Messages,
|
|
40
|
+
tools: Optional[ToolDefinitions] = None,
|
|
41
|
+
) -> AsyncGenerator[StreamChunk, None]:
|
|
42
|
+
"""
|
|
43
|
+
流式对话接口
|
|
44
|
+
|
|
45
|
+
:param messages: 消息列表
|
|
46
|
+
:param tools: 工具列表
|
|
47
|
+
:yield: StreamChunk 子类实例,调用方通过 isinstance 分支处理
|
|
48
|
+
:raise: LLMError
|
|
49
|
+
"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
# ---------- 工具方法 ----------
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def chunks_to_message(chunks: list[StreamChunk]) -> Message:
|
|
56
|
+
"""
|
|
57
|
+
将流式 chunks 聚合为一条完整的 assistant Message
|
|
58
|
+
|
|
59
|
+
遍历所有 chunk,拼接文本内容并收集工具调用,
|
|
60
|
+
最终产出一条可直接追加到对话历史的 assistant 消息。
|
|
61
|
+
|
|
62
|
+
:param chunks: 完整的流式 chunk 列表
|
|
63
|
+
:return: role=assistant 的 Message
|
|
64
|
+
"""
|
|
65
|
+
text_parts: list[str] = []
|
|
66
|
+
tool_calls: list[ToolCall] = []
|
|
67
|
+
|
|
68
|
+
for chunk in chunks:
|
|
69
|
+
if isinstance(chunk, TextChunk):
|
|
70
|
+
text_parts.append(chunk.text)
|
|
71
|
+
elif isinstance(chunk, ToolCallCompleteChunk):
|
|
72
|
+
tool_calls.extend(chunk.tool_calls)
|
|
73
|
+
|
|
74
|
+
content = ''.join(text_parts) if text_parts else None
|
|
75
|
+
return Message.assistant(
|
|
76
|
+
content=content,
|
|
77
|
+
tool_calls=tool_calls if tool_calls else None,
|
|
78
|
+
)
|
llm/clients/config.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""LLM 客户端配置模型与工厂"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ConnectionConfig(BaseModel):
|
|
7
|
+
"""HTTP 连接配置"""
|
|
8
|
+
|
|
9
|
+
timeout: float = Field(default=60.0, gt=0, description='请求读超时(秒)')
|
|
10
|
+
connect_timeout: float = Field(default=10.0, gt=0, description='连接超时(秒)')
|
|
11
|
+
|
|
12
|
+
max_retries: int = Field(default=3, ge=0, description='最大重试次数(0 表示不重试)')
|
|
13
|
+
base_delay: float = Field(default=1.0, gt=0, description='基础退避延迟(秒)')
|
|
14
|
+
max_delay: float = Field(default=30.0, gt=0, description='退避延迟上限(秒)')
|
|
15
|
+
jitter: bool = Field(default=True, description='是否启用 ±25% 随机抖动')
|
|
16
|
+
retryable_status: frozenset[int] = Field(
|
|
17
|
+
default=frozenset({429, 500, 502, 503, 504}),
|
|
18
|
+
description='可触发重试的 HTTP 状态码集合',
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LLMConfig(BaseModel):
|
|
23
|
+
"""LLM 客户端通用配置基类"""
|
|
24
|
+
|
|
25
|
+
provider: str = Field(description='服务提供商标识(如 openai)')
|
|
26
|
+
model: str = Field(description='模型名称')
|
|
27
|
+
include_usage: bool = Field(default=True, description='是否请求 token 使用统计')
|
|
28
|
+
connection: ConnectionConfig = Field(default_factory=ConnectionConfig, description='HTTP 连接与重试配置')
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""LLM 通用异常层级"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LLMError(Exception):
|
|
7
|
+
"""LLM 基础异常"""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LLMConfigError(LLMError):
|
|
13
|
+
"""配置/注册错误"""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LLMRequestError(LLMError):
|
|
19
|
+
"""LLM 请求失败的基类"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
message: str,
|
|
24
|
+
original_error: Optional[Exception] = None,
|
|
25
|
+
status_code: Optional[int] = None,
|
|
26
|
+
response_body: Optional[str] = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.original_error = original_error
|
|
30
|
+
self.status_code = status_code
|
|
31
|
+
self.response_body = response_body
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LLMNetworkError(LLMRequestError):
|
|
35
|
+
"""网络层错误:连接失败、超时、DNS 解析等"""
|
|
36
|
+
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LLMHTTPError(LLMRequestError):
|
|
41
|
+
"""HTTP 响应状态码非 200 的错误(基类)"""
|
|
42
|
+
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LLMAuthError(LLMHTTPError):
|
|
47
|
+
"""认证失败:401 或 403"""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LLMInsufficientBalanceError(LLMHTTPError):
|
|
53
|
+
"""余额不足:402"""
|
|
54
|
+
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class LLMRateLimitError(LLMHTTPError):
|
|
59
|
+
"""速率限制:429"""
|
|
60
|
+
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class LLMServerError(LLMHTTPError):
|
|
65
|
+
"""服务端错误:5xx"""
|
|
66
|
+
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LLMContentFilterError(LLMHTTPError):
|
|
71
|
+
"""内容安全过滤"""
|
|
72
|
+
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class LLMContextLengthError(LLMHTTPError):
|
|
77
|
+
"""上下文长度超出模型限制"""
|
|
78
|
+
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class LLMStreamError(LLMRequestError):
|
|
83
|
+
"""流式响应解析错误"""
|
|
84
|
+
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_http_error(
|
|
89
|
+
status_code: int,
|
|
90
|
+
message: Optional[str] = None,
|
|
91
|
+
original_error: Optional[Exception] = None,
|
|
92
|
+
response_body: Optional[str] = None,
|
|
93
|
+
) -> LLMHTTPError:
|
|
94
|
+
"""根据 HTTP 状态码构建对应的异常实例。"""
|
|
95
|
+
if message is None:
|
|
96
|
+
message = f'HTTP {status_code}'
|
|
97
|
+
|
|
98
|
+
if status_code in (401, 403):
|
|
99
|
+
return LLMAuthError(
|
|
100
|
+
message=message,
|
|
101
|
+
status_code=status_code,
|
|
102
|
+
original_error=original_error,
|
|
103
|
+
response_body=response_body,
|
|
104
|
+
)
|
|
105
|
+
elif status_code == 402:
|
|
106
|
+
return LLMInsufficientBalanceError(
|
|
107
|
+
message=message, status_code=status_code, original_error=original_error, response_body=response_body
|
|
108
|
+
)
|
|
109
|
+
elif status_code == 429:
|
|
110
|
+
return LLMRateLimitError(
|
|
111
|
+
message=message,
|
|
112
|
+
status_code=status_code,
|
|
113
|
+
original_error=original_error,
|
|
114
|
+
response_body=response_body,
|
|
115
|
+
)
|
|
116
|
+
elif 500 <= status_code < 600:
|
|
117
|
+
return LLMServerError(
|
|
118
|
+
message=message,
|
|
119
|
+
status_code=status_code,
|
|
120
|
+
original_error=original_error,
|
|
121
|
+
response_body=response_body,
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
return LLMHTTPError(
|
|
125
|
+
message=message,
|
|
126
|
+
status_code=status_code,
|
|
127
|
+
original_error=original_error,
|
|
128
|
+
response_body=response_body,
|
|
129
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .aggregation import aggregate_tool_call_deltas
|
|
2
|
+
from .client import OpenAIClient
|
|
3
|
+
from .config import OpenAIConfig, create_openai_client
|
|
4
|
+
from .connection import ConnectionManager
|
|
5
|
+
from .open_ai_models import (
|
|
6
|
+
Choice,
|
|
7
|
+
CompletionUsage,
|
|
8
|
+
Delta,
|
|
9
|
+
StreamEvent,
|
|
10
|
+
ToolCallDelta,
|
|
11
|
+
)
|
|
12
|
+
from .response_format import JsonSchema, ResponseFormat
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'OpenAIClient',
|
|
16
|
+
'OpenAIConfig',
|
|
17
|
+
'create_openai_client',
|
|
18
|
+
'ConnectionManager',
|
|
19
|
+
'aggregate_tool_call_deltas',
|
|
20
|
+
'ResponseFormat',
|
|
21
|
+
'JsonSchema',
|
|
22
|
+
'Choice',
|
|
23
|
+
'CompletionUsage',
|
|
24
|
+
'Delta',
|
|
25
|
+
'StreamEvent',
|
|
26
|
+
'ToolCallDelta',
|
|
27
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""OpenAI 流式 tool_calls 增量片段的聚合工具"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from ...models import FunctionCall, ToolCall
|
|
9
|
+
from .open_ai_models import ToolCallDelta
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _PendingToolCall(BaseModel):
|
|
13
|
+
"""工具调用聚合过程中的暂存状态"""
|
|
14
|
+
|
|
15
|
+
id: Optional[str] = Field(default=None, description='工具调用唯一标识符')
|
|
16
|
+
function_name: Optional[str] = Field(default=None, description='函数名称')
|
|
17
|
+
function_arguments: str = Field(default='', description='已拼接的 JSON 参数字符串')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def aggregate_tool_call_deltas(
|
|
21
|
+
tool_call_deltas: List[ToolCallDelta],
|
|
22
|
+
) -> List[ToolCall]:
|
|
23
|
+
"""
|
|
24
|
+
将流式 tool_calls 增量片段聚合为完整的 ToolCall 列表。
|
|
25
|
+
|
|
26
|
+
OpenAI 流式协议中,每个 tool_call delta 包含 index 字段,
|
|
27
|
+
同一 index 的多个 delta 需要拼接 arguments 字符串后解析为 dict。
|
|
28
|
+
"""
|
|
29
|
+
pending: Dict[int, _PendingToolCall] = {}
|
|
30
|
+
for tc in tool_call_deltas:
|
|
31
|
+
idx: int = tc.index
|
|
32
|
+
if idx not in pending:
|
|
33
|
+
pending[idx] = _PendingToolCall()
|
|
34
|
+
entry = pending[idx]
|
|
35
|
+
if tc.id:
|
|
36
|
+
entry.id = tc.id
|
|
37
|
+
func = tc.function
|
|
38
|
+
if func is not None:
|
|
39
|
+
if func.name:
|
|
40
|
+
entry.function_name = func.name
|
|
41
|
+
entry.function_arguments += func.arguments
|
|
42
|
+
|
|
43
|
+
result: List[ToolCall] = []
|
|
44
|
+
for idx in sorted(pending):
|
|
45
|
+
entry = pending[idx]
|
|
46
|
+
tool_id: Optional[str] = entry.id
|
|
47
|
+
tool_name: Optional[str] = entry.function_name
|
|
48
|
+
if tool_id is None or tool_name is None:
|
|
49
|
+
continue
|
|
50
|
+
try:
|
|
51
|
+
arguments: Dict[str, object] = json.loads(entry.function_arguments) if entry.function_arguments else {}
|
|
52
|
+
except json.JSONDecodeError:
|
|
53
|
+
arguments = {}
|
|
54
|
+
result.append(
|
|
55
|
+
ToolCall(
|
|
56
|
+
id=tool_id,
|
|
57
|
+
function=FunctionCall(
|
|
58
|
+
name=tool_name,
|
|
59
|
+
arguments=arguments,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
return result
|