provider-sdk 0.1.0__tar.gz
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.
- provider_sdk-0.1.0/LICENSE +21 -0
- provider_sdk-0.1.0/PKG-INFO +103 -0
- provider_sdk-0.1.0/README.md +74 -0
- provider_sdk-0.1.0/provider_sdk/__init__.py +39 -0
- provider_sdk-0.1.0/provider_sdk/capabilities/__init__.py +73 -0
- provider_sdk-0.1.0/provider_sdk/config.py +78 -0
- provider_sdk-0.1.0/provider_sdk/context.py +78 -0
- provider_sdk-0.1.0/provider_sdk/integrate.py +51 -0
- provider_sdk-0.1.0/provider_sdk/platform.py +140 -0
- provider_sdk-0.1.0/provider_sdk/plugin.py +172 -0
- provider_sdk-0.1.0/provider_sdk/runtime/__init__.py +15 -0
- provider_sdk-0.1.0/provider_sdk/runtime/loader.py +264 -0
- provider_sdk-0.1.0/provider_sdk/types/__init__.py +14 -0
- provider_sdk-0.1.0/provider_sdk/types/candidate.py +138 -0
- provider_sdk-0.1.0/provider_sdk/types/manifest.py +108 -0
- provider_sdk-0.1.0/provider_sdk.egg-info/PKG-INFO +103 -0
- provider_sdk-0.1.0/provider_sdk.egg-info/SOURCES.txt +22 -0
- provider_sdk-0.1.0/provider_sdk.egg-info/dependency_links.txt +1 -0
- provider_sdk-0.1.0/provider_sdk.egg-info/requires.txt +6 -0
- provider_sdk-0.1.0/provider_sdk.egg-info/top_level.txt +1 -0
- provider_sdk-0.1.0/pyproject.toml +48 -0
- provider_sdk-0.1.0/setup.cfg +4 -0
- provider_sdk-0.1.0/tests/test_config.py +36 -0
- provider_sdk-0.1.0/tests/test_loader.py +60 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 provider-sdk contributors
|
|
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,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: provider-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Provider-V2 平台插件 SDK — 第三方平台适配器开发与加载
|
|
5
|
+
Author: provider-sdk
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nichengfuben/provider-sdk
|
|
8
|
+
Project-URL: Documentation, https://github.com/nichengfuben/provider-sdk/blob/main/docs/guide.md
|
|
9
|
+
Project-URL: Repository, https://github.com/nichengfuben/provider-sdk
|
|
10
|
+
Project-URL: Issues, https://github.com/nichengfuben/provider-sdk/issues
|
|
11
|
+
Keywords: provider,llm,gateway,plugin,sdk,openai
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# provider-sdk
|
|
31
|
+
|
|
32
|
+
Provider-V2 平台插件 SDK。对标 [maibot-plugin-sdk](https://github.com/Mai-with-u/maibot-plugin-sdk) 的插件契约,为第三方**平台适配器**提供独立开发与加载能力。
|
|
33
|
+
|
|
34
|
+
## 安装
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install -e X:\Project\provider-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 快速开始
|
|
41
|
+
|
|
42
|
+
在 Provider Host 的 `plugins/` 目录下创建插件文件夹:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
plugins/
|
|
46
|
+
my_platform/
|
|
47
|
+
_manifest.json
|
|
48
|
+
plugin.py
|
|
49
|
+
config.toml # 可选,由 Host 管理
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`plugin.py` 最小示例见 [`examples/echo_platform/plugin.py`](examples/echo_platform/plugin.py)。
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from provider_sdk import ProviderPlugin, PlatformAdapter, Candidate, make_id
|
|
56
|
+
|
|
57
|
+
class MyPlugin(ProviderPlugin, PlatformAdapter):
|
|
58
|
+
@property
|
|
59
|
+
def name(self) -> str:
|
|
60
|
+
return "my_platform"
|
|
61
|
+
|
|
62
|
+
async def on_load(self) -> None:
|
|
63
|
+
self.ctx.logger.info("loaded")
|
|
64
|
+
|
|
65
|
+
async def init(self, session): ...
|
|
66
|
+
async def candidates(self) -> list[Candidate]: ...
|
|
67
|
+
async def ensure_candidates(self, count: int) -> int: ...
|
|
68
|
+
async def complete(self, candidate, messages, model, stream, **kw): ...
|
|
69
|
+
async def close(self) -> None: ...
|
|
70
|
+
|
|
71
|
+
def create_plugin() -> MyPlugin:
|
|
72
|
+
return MyPlugin()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 与 MaiBot SDK 的对应关系
|
|
76
|
+
|
|
77
|
+
| MaiBot | provider-sdk |
|
|
78
|
+
|--------|----------------|
|
|
79
|
+
| `MaiBotPlugin` | `ProviderPlugin` |
|
|
80
|
+
| `create_plugin()` | `create_plugin()` |
|
|
81
|
+
| `_manifest.json` | `_manifest.json` |
|
|
82
|
+
| `on_load` / `on_unload` / `on_config_update` | 相同 |
|
|
83
|
+
| `self.ctx.send` / `gateway` 等 | `self.ctx.config` / `http` / `logger` |
|
|
84
|
+
| `@Tool` / `@Command` | 平台插件以 `PlatformAdapter` 能力为主 |
|
|
85
|
+
|
|
86
|
+
## Host 集成
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from pathlib import Path
|
|
90
|
+
from provider_sdk.integrate import load_platform_plugins, register_loaded_plugins
|
|
91
|
+
|
|
92
|
+
loaded = await load_platform_plugins(Path("plugins"), session)
|
|
93
|
+
register_loaded_plugins(registry, loaded)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
完整说明见 [`docs/guide.md`](docs/guide.md)。
|
|
97
|
+
|
|
98
|
+
## 开发
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install -e ".[dev]"
|
|
102
|
+
pytest -q
|
|
103
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# provider-sdk
|
|
2
|
+
|
|
3
|
+
Provider-V2 平台插件 SDK。对标 [maibot-plugin-sdk](https://github.com/Mai-with-u/maibot-plugin-sdk) 的插件契约,为第三方**平台适配器**提供独立开发与加载能力。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e X:\Project\provider-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
在 Provider Host 的 `plugins/` 目录下创建插件文件夹:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
plugins/
|
|
17
|
+
my_platform/
|
|
18
|
+
_manifest.json
|
|
19
|
+
plugin.py
|
|
20
|
+
config.toml # 可选,由 Host 管理
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`plugin.py` 最小示例见 [`examples/echo_platform/plugin.py`](examples/echo_platform/plugin.py)。
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from provider_sdk import ProviderPlugin, PlatformAdapter, Candidate, make_id
|
|
27
|
+
|
|
28
|
+
class MyPlugin(ProviderPlugin, PlatformAdapter):
|
|
29
|
+
@property
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
return "my_platform"
|
|
32
|
+
|
|
33
|
+
async def on_load(self) -> None:
|
|
34
|
+
self.ctx.logger.info("loaded")
|
|
35
|
+
|
|
36
|
+
async def init(self, session): ...
|
|
37
|
+
async def candidates(self) -> list[Candidate]: ...
|
|
38
|
+
async def ensure_candidates(self, count: int) -> int: ...
|
|
39
|
+
async def complete(self, candidate, messages, model, stream, **kw): ...
|
|
40
|
+
async def close(self) -> None: ...
|
|
41
|
+
|
|
42
|
+
def create_plugin() -> MyPlugin:
|
|
43
|
+
return MyPlugin()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 与 MaiBot SDK 的对应关系
|
|
47
|
+
|
|
48
|
+
| MaiBot | provider-sdk |
|
|
49
|
+
|--------|----------------|
|
|
50
|
+
| `MaiBotPlugin` | `ProviderPlugin` |
|
|
51
|
+
| `create_plugin()` | `create_plugin()` |
|
|
52
|
+
| `_manifest.json` | `_manifest.json` |
|
|
53
|
+
| `on_load` / `on_unload` / `on_config_update` | 相同 |
|
|
54
|
+
| `self.ctx.send` / `gateway` 等 | `self.ctx.config` / `http` / `logger` |
|
|
55
|
+
| `@Tool` / `@Command` | 平台插件以 `PlatformAdapter` 能力为主 |
|
|
56
|
+
|
|
57
|
+
## Host 集成
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
from provider_sdk.integrate import load_platform_plugins, register_loaded_plugins
|
|
62
|
+
|
|
63
|
+
loaded = await load_platform_plugins(Path("plugins"), session)
|
|
64
|
+
register_loaded_plugins(registry, loaded)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
完整说明见 [`docs/guide.md`](docs/guide.md)。
|
|
68
|
+
|
|
69
|
+
## 开发
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install -e ".[dev]"
|
|
73
|
+
pytest -q
|
|
74
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Provider-V2 平台插件 SDK。
|
|
2
|
+
|
|
3
|
+
插件作者的唯一依赖入口。插件不得导入 ``src.*``,只能通过本 SDK 与 Host 交互。
|
|
4
|
+
|
|
5
|
+
核心导出:
|
|
6
|
+
|
|
7
|
+
- :class:`ProviderPlugin` — 插件基类(生命周期 + 配置)
|
|
8
|
+
- :class:`PlatformAdapter` — 平台适配器抽象接口
|
|
9
|
+
- :class:`PluginContext` — 运行时上下文(日志 / 配置 / HTTP)
|
|
10
|
+
- :class:`PluginConfigBase` — 插件配置模型基类
|
|
11
|
+
- :class:`Candidate` — 网关候选项
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from provider_sdk.config import Field, PluginConfigBase
|
|
15
|
+
from provider_sdk.context import PluginContext
|
|
16
|
+
from provider_sdk.platform import PlatformAdapter
|
|
17
|
+
from provider_sdk.plugin import ProviderPlugin
|
|
18
|
+
from provider_sdk.types import (
|
|
19
|
+
CONFIG_RELOAD_SCOPE_GLOBAL,
|
|
20
|
+
CONFIG_RELOAD_SCOPE_SELF,
|
|
21
|
+
ALL_CAPABILITIES,
|
|
22
|
+
)
|
|
23
|
+
from provider_sdk.types.candidate import Candidate, make_id
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ProviderPlugin",
|
|
29
|
+
"PlatformAdapter",
|
|
30
|
+
"PluginContext",
|
|
31
|
+
"PluginConfigBase",
|
|
32
|
+
"Field",
|
|
33
|
+
"Candidate",
|
|
34
|
+
"make_id",
|
|
35
|
+
"CONFIG_RELOAD_SCOPE_SELF",
|
|
36
|
+
"CONFIG_RELOAD_SCOPE_GLOBAL",
|
|
37
|
+
"ALL_CAPABILITIES",
|
|
38
|
+
"__version__",
|
|
39
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""能力代理:配置、HTTP、日志。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
|
|
11
|
+
__all__ = ["ConfigCapability", "HttpCapability", "LoggingCapability"]
|
|
12
|
+
|
|
13
|
+
GetConfigFn = Callable[[str, str], Any]
|
|
14
|
+
GetGlobalConfigFn = Callable[[], Mapping[str, Any]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LoggingCapability:
|
|
18
|
+
"""插件日志能力。"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, logger: logging.Logger) -> None:
|
|
21
|
+
self._logger = logger
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def logger(self) -> logging.Logger:
|
|
25
|
+
return self._logger
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConfigCapability:
|
|
29
|
+
"""插件配置读取能力。"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
get_plugin_config: Callable[[], Mapping[str, Any]],
|
|
35
|
+
get_global_config: Optional[GetGlobalConfigFn] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
self._get_plugin_config = get_plugin_config
|
|
38
|
+
self._get_global_config = get_global_config
|
|
39
|
+
|
|
40
|
+
def get_plugin(self) -> dict[str, Any]:
|
|
41
|
+
"""返回当前插件配置字典副本。"""
|
|
42
|
+
raw = self._get_plugin_config()
|
|
43
|
+
return dict(raw) if isinstance(raw, Mapping) else {}
|
|
44
|
+
|
|
45
|
+
def get_global(self) -> dict[str, Any]:
|
|
46
|
+
"""返回 Host 全局配置字典副本。"""
|
|
47
|
+
if self._get_global_config is None:
|
|
48
|
+
return {}
|
|
49
|
+
raw = self._get_global_config()
|
|
50
|
+
return dict(raw) if isinstance(raw, Mapping) else {}
|
|
51
|
+
|
|
52
|
+
def get(self, section: str, key: str, default: Any = None) -> Any:
|
|
53
|
+
"""读取全局配置中的嵌套键。"""
|
|
54
|
+
data = self.get_global()
|
|
55
|
+
node = data.get(section)
|
|
56
|
+
if isinstance(node, Mapping):
|
|
57
|
+
return node.get(key, default)
|
|
58
|
+
return default
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class HttpCapability:
|
|
62
|
+
"""共享 HTTP 会话能力。"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, session: aiohttp.ClientSession) -> None:
|
|
65
|
+
self._session = session
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def session(self) -> aiohttp.ClientSession:
|
|
69
|
+
return self._session
|
|
70
|
+
|
|
71
|
+
async def request(self, method: str, url: str, **kwargs: Any) -> aiohttp.ClientResponse:
|
|
72
|
+
"""对共享会话发起请求。"""
|
|
73
|
+
return await self._session.request(method, url, **kwargs)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""插件配置模型与校验工具。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any, ClassVar, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Field",
|
|
13
|
+
"PluginConfigBase",
|
|
14
|
+
"build_plugin_default_config",
|
|
15
|
+
"is_plugin_config_class",
|
|
16
|
+
"merge_plugin_config_data",
|
|
17
|
+
"validate_plugin_config",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
PluginConfigT = TypeVar("PluginConfigT", bound="PluginConfigBase")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PluginConfigBase(BaseModel):
|
|
24
|
+
"""插件配置模型基类。
|
|
25
|
+
|
|
26
|
+
插件作者继承此类声明配置结构;Host 据此生成默认配置与 WebUI Schema。
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(validate_assignment=True, extra="ignore")
|
|
30
|
+
|
|
31
|
+
__ui_label__: ClassVar[str] = ""
|
|
32
|
+
__ui_icon__: ClassVar[str] = ""
|
|
33
|
+
__ui_order__: ClassVar[int] = 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_plugin_config_class(candidate: Any) -> bool:
|
|
37
|
+
"""判断对象是否为插件配置模型类。"""
|
|
38
|
+
return bool(inspect.isclass(candidate) and issubclass(candidate, PluginConfigBase))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_plugin_default_config(config_class: type[PluginConfigT]) -> dict[str, Any]:
|
|
42
|
+
"""根据配置模型构造默认配置字典。"""
|
|
43
|
+
try:
|
|
44
|
+
instance = config_class()
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"插件配置模型 {config_class.__name__} 需要为所有字段提供默认值"
|
|
48
|
+
) from exc
|
|
49
|
+
return instance.model_dump(mode="python")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def validate_plugin_config(
|
|
53
|
+
config_class: type[PluginConfigT],
|
|
54
|
+
config_data: Mapping[str, Any],
|
|
55
|
+
) -> PluginConfigT:
|
|
56
|
+
"""校验并构造强类型配置实例。"""
|
|
57
|
+
return config_class.model_validate(dict(config_data))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def merge_plugin_config_data(
|
|
61
|
+
default_config: Mapping[str, Any],
|
|
62
|
+
raw_config: Mapping[str, Any],
|
|
63
|
+
) -> tuple[dict[str, Any], bool]:
|
|
64
|
+
"""将用户配置递归合并到默认配置之上。"""
|
|
65
|
+
changed = False
|
|
66
|
+
merged: dict[str, Any] = dict(default_config)
|
|
67
|
+
|
|
68
|
+
for key, value in raw_config.items():
|
|
69
|
+
base_value = merged.get(key)
|
|
70
|
+
if isinstance(base_value, Mapping) and isinstance(value, Mapping):
|
|
71
|
+
nested, nested_changed = merge_plugin_config_data(base_value, value)
|
|
72
|
+
merged[key] = nested
|
|
73
|
+
changed = changed or nested_changed or nested != dict(base_value)
|
|
74
|
+
elif base_value != value:
|
|
75
|
+
merged[key] = value
|
|
76
|
+
changed = True
|
|
77
|
+
|
|
78
|
+
return merged, changed
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""插件运行时上下文。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
|
|
12
|
+
from provider_sdk.capabilities import ConfigCapability, HttpCapability, LoggingCapability
|
|
13
|
+
|
|
14
|
+
__all__ = ["HostServices", "PluginContext", "create_plugin_context"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class HostServices:
|
|
19
|
+
"""Host 注入给 SDK 的运行时服务集合。"""
|
|
20
|
+
|
|
21
|
+
session: aiohttp.ClientSession
|
|
22
|
+
plugin_id: str
|
|
23
|
+
plugin_dir: str = ""
|
|
24
|
+
get_plugin_config: Any = None
|
|
25
|
+
get_global_config: Any = None
|
|
26
|
+
logger: Optional[logging.Logger] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PluginContext:
|
|
30
|
+
"""插件运行时上下文。
|
|
31
|
+
|
|
32
|
+
插件通过 ``self.ctx`` 访问日志、配置与 HTTP 会话。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
plugin_id: str,
|
|
39
|
+
plugin_dir: str,
|
|
40
|
+
logger: logging.Logger,
|
|
41
|
+
config: ConfigCapability,
|
|
42
|
+
http: HttpCapability,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.plugin_id = plugin_id
|
|
45
|
+
self.plugin_dir = plugin_dir
|
|
46
|
+
self.logger = logger
|
|
47
|
+
self.config = config
|
|
48
|
+
self.http = http
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def paths(self) -> Mapping[str, str]:
|
|
52
|
+
"""插件相关路径。"""
|
|
53
|
+
return {"plugin_dir": self.plugin_dir}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def create_plugin_context(services: HostServices) -> PluginContext:
|
|
57
|
+
"""根据 Host 服务构造插件上下文。"""
|
|
58
|
+
plugin_id = services.plugin_id
|
|
59
|
+
logger = services.logger or logging.getLogger(f"provider_sdk.plugin.{plugin_id}")
|
|
60
|
+
|
|
61
|
+
def _plugin_config() -> Mapping[str, Any]:
|
|
62
|
+
if callable(services.get_plugin_config):
|
|
63
|
+
raw = services.get_plugin_config()
|
|
64
|
+
return dict(raw) if isinstance(raw, Mapping) else {}
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
config_cap = ConfigCapability(
|
|
68
|
+
get_plugin_config=_plugin_config,
|
|
69
|
+
get_global_config=services.get_global_config if callable(services.get_global_config) else None,
|
|
70
|
+
)
|
|
71
|
+
http_cap = HttpCapability(services.session)
|
|
72
|
+
return PluginContext(
|
|
73
|
+
plugin_id=plugin_id,
|
|
74
|
+
plugin_dir=services.plugin_dir,
|
|
75
|
+
logger=logger,
|
|
76
|
+
config=config_cap,
|
|
77
|
+
http=http_cap,
|
|
78
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""与 Provider Host 注册表集成的辅助函数。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Callable, Mapping, Optional, Sequence
|
|
7
|
+
|
|
8
|
+
import aiohttp
|
|
9
|
+
|
|
10
|
+
from provider_sdk.runtime.loader import LoadedPlugin, PluginLoader
|
|
11
|
+
|
|
12
|
+
__all__ = ["load_platform_plugins", "register_loaded_plugins"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def load_platform_plugins(
|
|
16
|
+
plugins_root: Path,
|
|
17
|
+
session: aiohttp.ClientSession,
|
|
18
|
+
*,
|
|
19
|
+
host_version: str = "",
|
|
20
|
+
get_plugin_config: Optional[Callable[[str], Mapping[str, Any]]] = None,
|
|
21
|
+
get_global_config: Optional[Callable[[], Mapping[str, Any]]] = None,
|
|
22
|
+
whitelist: Optional[Sequence[str]] = None,
|
|
23
|
+
blacklist: Optional[Sequence[str]] = None,
|
|
24
|
+
) -> list[LoadedPlugin]:
|
|
25
|
+
"""发现并加载 ``plugins/`` 目录下的平台插件。"""
|
|
26
|
+
loader = PluginLoader(host_version=host_version, plugin_type_filter="platform")
|
|
27
|
+
return await loader.discover_and_load(
|
|
28
|
+
plugins_root,
|
|
29
|
+
session,
|
|
30
|
+
get_plugin_config=get_plugin_config,
|
|
31
|
+
get_global_config=get_global_config,
|
|
32
|
+
whitelist=whitelist,
|
|
33
|
+
blacklist=blacklist,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def register_loaded_plugins(registry: Any, loaded: Sequence[LoadedPlugin]) -> None:
|
|
38
|
+
"""将已加载插件的适配器注册到 echotools 风格注册表。
|
|
39
|
+
|
|
40
|
+
``registry`` 需实现 ``register(adapter)`` 或内部 ``_registry.register``。
|
|
41
|
+
"""
|
|
42
|
+
for record in loaded:
|
|
43
|
+
adapter = record.adapter
|
|
44
|
+
if hasattr(registry, "register") and callable(registry.register):
|
|
45
|
+
registry.register(adapter)
|
|
46
|
+
continue
|
|
47
|
+
inner = getattr(registry, "_registry", None)
|
|
48
|
+
if inner is not None and hasattr(inner, "register"):
|
|
49
|
+
inner.register(adapter)
|
|
50
|
+
continue
|
|
51
|
+
raise TypeError("registry 不支持 register(adapter)")
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""平台适配器抽象接口 — 与 Provider Host 内置适配器契约一致。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
|
|
12
|
+
from provider_sdk.types.candidate import Candidate
|
|
13
|
+
|
|
14
|
+
__all__ = ["PlatformAdapter", "DEFAULT_CONTEXT_LENGTH"]
|
|
15
|
+
|
|
16
|
+
DEFAULT_CONTEXT_LENGTH = 131072
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PlatformAdapter(ABC):
|
|
20
|
+
"""所有平台适配器的抽象基类。
|
|
21
|
+
|
|
22
|
+
设计原则(与 Host 一致):
|
|
23
|
+
|
|
24
|
+
- ``init()`` 必须立即返回,耗时操作放后台 Task
|
|
25
|
+
- ``candidates()`` 随时反映真实状态
|
|
26
|
+
- 可选能力方法有安全默认实现
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
"""平台标识名(小写,建议与目录名一致)。"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def supported_models(self) -> List[str]:
|
|
37
|
+
"""支持的模型列表。"""
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def default_capabilities(self) -> Dict[str, bool]:
|
|
42
|
+
"""默认能力字典,用于 ``/v1/models`` 输出。"""
|
|
43
|
+
return {"chat": True}
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def context_length(self) -> Optional[int]:
|
|
47
|
+
"""默认上下文长度。"""
|
|
48
|
+
return DEFAULT_CONTEXT_LENGTH
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
async def init(self, session: aiohttp.ClientSession) -> None:
|
|
52
|
+
"""初始化适配器,必须立即返回。"""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
async def candidates(self) -> List[Candidate]:
|
|
57
|
+
"""返回当前可用候选项列表。"""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
async def ensure_candidates(self, count: int) -> int:
|
|
62
|
+
"""确保至少有 ``count`` 个候选项可用。"""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
async def complete(
|
|
67
|
+
self,
|
|
68
|
+
candidate: Candidate,
|
|
69
|
+
messages: List[Dict[str, Any]],
|
|
70
|
+
model: str,
|
|
71
|
+
stream: bool,
|
|
72
|
+
*,
|
|
73
|
+
thinking: bool = False,
|
|
74
|
+
search: bool = False,
|
|
75
|
+
**kw: Any,
|
|
76
|
+
) -> AsyncGenerator[Union[str, Dict[str, Any]], None]:
|
|
77
|
+
"""聊天补全,yield 文本增量或结构化事件。"""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
async def close(self) -> None:
|
|
82
|
+
"""关闭适配器并释放资源。"""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
async def fetch_remote_models(self) -> List[str]:
|
|
86
|
+
"""拉取远程模型列表。"""
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
async def create_embedding(
|
|
90
|
+
self,
|
|
91
|
+
candidate: Candidate,
|
|
92
|
+
input_data: Union[str, List[str]],
|
|
93
|
+
model: str,
|
|
94
|
+
**kw: Any,
|
|
95
|
+
) -> Dict[str, Any]:
|
|
96
|
+
inputs = [input_data] if isinstance(input_data, str) else list(input_data)
|
|
97
|
+
return {
|
|
98
|
+
"object": "list",
|
|
99
|
+
"data": [
|
|
100
|
+
{"object": "embedding", "index": i, "embedding": []}
|
|
101
|
+
for i, _ in enumerate(inputs)
|
|
102
|
+
],
|
|
103
|
+
"model": model,
|
|
104
|
+
"usage": {"prompt_tokens": 0, "total_tokens": 0},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async def create_image(
|
|
108
|
+
self,
|
|
109
|
+
candidate: Candidate,
|
|
110
|
+
prompt: str,
|
|
111
|
+
model: str,
|
|
112
|
+
**kw: Any,
|
|
113
|
+
) -> Dict[str, Any]:
|
|
114
|
+
return {"created": int(time.time()), "data": []}
|
|
115
|
+
|
|
116
|
+
async def create_moderation(
|
|
117
|
+
self,
|
|
118
|
+
candidate: Candidate,
|
|
119
|
+
input_data: Union[str, List[str]],
|
|
120
|
+
model: str,
|
|
121
|
+
**kw: Any,
|
|
122
|
+
) -> Dict[str, Any]:
|
|
123
|
+
inputs = [input_data] if isinstance(input_data, str) else list(input_data)
|
|
124
|
+
return {
|
|
125
|
+
"id": f"modr-{uuid.uuid4().hex[:24]}",
|
|
126
|
+
"model": model,
|
|
127
|
+
"results": [{"flagged": False, "categories": {}, "category_scores": {}} for _ in inputs],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def set_proxy_enabled(self, enabled: bool) -> None:
|
|
131
|
+
"""设置代理覆盖开关(可选)。"""
|
|
132
|
+
del enabled
|
|
133
|
+
|
|
134
|
+
def is_proxy_allowed(self) -> bool:
|
|
135
|
+
"""是否允许代理切换。"""
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def is_proxy_enabled(self) -> bool:
|
|
139
|
+
"""当前是否启用代理。"""
|
|
140
|
+
return False
|