provider-sdk 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.
@@ -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)
provider_sdk/config.py ADDED
@@ -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
provider_sdk/plugin.py ADDED
@@ -0,0 +1,172 @@
1
+ """Provider 平台插件基类。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Mapping
7
+ from typing import Any, ClassVar, Iterable
8
+
9
+ from provider_sdk.config import (
10
+ PluginConfigBase,
11
+ build_plugin_default_config,
12
+ is_plugin_config_class,
13
+ merge_plugin_config_data,
14
+ validate_plugin_config,
15
+ )
16
+ from provider_sdk.context import PluginContext
17
+ from provider_sdk.platform import PlatformAdapter
18
+ from provider_sdk.types import CONFIG_RELOAD_SCOPE_SELF
19
+
20
+ __all__ = ["ProviderPlugin", "get_platform_adapter", "is_platform_plugin"]
21
+
22
+
23
+ class ProviderPlugin:
24
+ """SDK 平台插件基类。
25
+
26
+ 用法示例::
27
+
28
+ class EchoPlugin(ProviderPlugin, PlatformAdapter):
29
+ @property
30
+ def name(self) -> str:
31
+ return "echo"
32
+
33
+ async def on_load(self) -> None:
34
+ self.ctx.logger.info("echo 插件已加载")
35
+
36
+ async def init(self, session):
37
+ ...
38
+
39
+ def create_plugin() -> EchoPlugin:
40
+ return EchoPlugin()
41
+ """
42
+
43
+ config_reload_subscriptions: ClassVar[Iterable[str]] = ()
44
+ config_model: ClassVar[type[PluginConfigBase] | None] = None
45
+
46
+ def __init__(self) -> None:
47
+ self._ctx: PluginContext | None = None
48
+ self._plugin_config_data: dict[str, Any] = {}
49
+ self._plugin_config_instance: PluginConfigBase | None = None
50
+
51
+ @classmethod
52
+ def get_config_model(cls) -> type[PluginConfigBase] | None:
53
+ candidate = cls.config_model
54
+ return candidate if is_plugin_config_class(candidate) else None
55
+
56
+ @classmethod
57
+ def has_config_model(cls) -> bool:
58
+ return cls.get_config_model() is not None
59
+
60
+ @classmethod
61
+ def build_default_config(cls) -> dict[str, Any]:
62
+ config_class = cls.get_config_model()
63
+ if config_class is None:
64
+ return {}
65
+ return build_plugin_default_config(config_class)
66
+
67
+ def normalize_plugin_config(
68
+ self,
69
+ config_data: Mapping[str, Any] | None,
70
+ ) -> tuple[dict[str, Any], bool]:
71
+ raw_config: dict[str, Any] = dict(config_data) if isinstance(config_data, Mapping) else {}
72
+ config_class = type(self).get_config_model()
73
+ if config_class is None:
74
+ return raw_config, False
75
+
76
+ default_config = type(self).build_default_config()
77
+ if not raw_config:
78
+ validated = validate_plugin_config(config_class, default_config)
79
+ return validated.model_dump(mode="python"), bool(default_config)
80
+
81
+ merged, changed = merge_plugin_config_data(default_config, raw_config)
82
+ validated = validate_plugin_config(config_class, merged)
83
+ normalized = validated.model_dump(mode="python")
84
+ return normalized, changed or normalized != merged
85
+
86
+ def set_plugin_config(self, config: dict[str, Any]) -> None:
87
+ normalized, _ = self.normalize_plugin_config(config)
88
+ self._plugin_config_data = normalized
89
+
90
+ config_class = type(self).get_config_model()
91
+ if config_class is None:
92
+ self._plugin_config_instance = None
93
+ return
94
+
95
+ try:
96
+ self._plugin_config_instance = validate_plugin_config(config_class, normalized)
97
+ except Exception as exc:
98
+ self._plugin_config_instance = None
99
+ self._get_logger().warning("插件配置校验失败: %s", exc)
100
+
101
+ @property
102
+ def config(self) -> PluginConfigBase:
103
+ if not type(self).has_config_model():
104
+ raise RuntimeError("当前插件未声明 config_model")
105
+ if self._plugin_config_instance is None:
106
+ raise RuntimeError("当前插件配置尚未注入")
107
+ return self._plugin_config_instance
108
+
109
+ def get_plugin_config_data(self) -> dict[str, Any]:
110
+ return dict(self._plugin_config_data)
111
+
112
+ @property
113
+ def ctx(self) -> PluginContext:
114
+ if self._ctx is None:
115
+ raise RuntimeError("插件上下文尚未初始化")
116
+ return self._ctx
117
+
118
+ def _set_context(self, ctx: PluginContext) -> None:
119
+ self._ctx = ctx
120
+
121
+ def _get_logger(self) -> logging.Logger:
122
+ if self._ctx is not None:
123
+ return self._ctx.logger
124
+ return logging.getLogger("provider_sdk.plugin")
125
+
126
+ async def on_load(self) -> None:
127
+ """插件加载完成后的生命周期钩子。"""
128
+
129
+ async def on_unload(self) -> None:
130
+ """插件卸载前的生命周期钩子。"""
131
+
132
+ async def on_config_update(
133
+ self,
134
+ scope: str,
135
+ config_data: dict[str, object],
136
+ version: str,
137
+ ) -> None:
138
+ """配置热重载回调。
139
+
140
+ ``scope`` 为 ``self`` 时表示插件自身 ``config.toml`` 变更。
141
+ """
142
+ if scope == CONFIG_RELOAD_SCOPE_SELF:
143
+ self.set_plugin_config(dict(config_data))
144
+ del version
145
+
146
+
147
+ def is_platform_plugin(instance: Any) -> bool:
148
+ """判断对象是否为合法的 Provider 平台插件。"""
149
+ return isinstance(instance, ProviderPlugin)
150
+
151
+
152
+ def get_platform_adapter(plugin: ProviderPlugin) -> PlatformAdapter:
153
+ """从插件实例提取平台适配器。
154
+
155
+ 插件类可同时继承 :class:`PlatformAdapter`;否则必须实现
156
+ ``get_adapter()`` 并返回适配器实例。
157
+
158
+ Raises:
159
+ TypeError: 插件未提供平台适配器。
160
+ """
161
+ if isinstance(plugin, PlatformAdapter):
162
+ return plugin
163
+
164
+ getter = getattr(plugin, "get_adapter", None)
165
+ if callable(getter):
166
+ adapter = getter()
167
+ if isinstance(adapter, PlatformAdapter):
168
+ return adapter
169
+
170
+ raise TypeError(
171
+ f"插件 {type(plugin).__name__} 必须继承 PlatformAdapter 或实现 get_adapter()"
172
+ )
@@ -0,0 +1,15 @@
1
+ """Host 侧插件运行时。"""
2
+
3
+ from provider_sdk.runtime.loader import (
4
+ LoadedPlugin,
5
+ PluginLoadError,
6
+ PluginLoader,
7
+ discover_plugin_dirs,
8
+ )
9
+
10
+ __all__ = [
11
+ "LoadedPlugin",
12
+ "PluginLoadError",
13
+ "PluginLoader",
14
+ "discover_plugin_dirs",
15
+ ]
@@ -0,0 +1,264 @@
1
+ """Host 侧插件发现与加载。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import logging
7
+ import sys
8
+ from collections import deque
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence
12
+
13
+ import aiohttp
14
+
15
+ from provider_sdk.context import HostServices, create_plugin_context
16
+ from provider_sdk.plugin import ProviderPlugin, get_platform_adapter, is_platform_plugin
17
+ from provider_sdk.platform import PlatformAdapter
18
+ from provider_sdk.types.manifest import PluginManifest, load_manifest_file
19
+
20
+ __all__ = [
21
+ "LoadedPlugin",
22
+ "PluginLoadError",
23
+ "PluginLoader",
24
+ "discover_plugin_dirs",
25
+ ]
26
+
27
+ logger = logging.getLogger("provider_sdk.runtime.loader")
28
+
29
+ _MANIFEST_NAME = "_manifest.json"
30
+ _ENTRY_MODULE = "plugin.py"
31
+ _FACTORY_NAME = "create_plugin"
32
+
33
+
34
+ class PluginLoadError(RuntimeError):
35
+ """插件加载失败。"""
36
+
37
+
38
+ @dataclass
39
+ class LoadedPlugin:
40
+ """加载成功的插件记录。"""
41
+
42
+ manifest: PluginManifest
43
+ plugin: ProviderPlugin
44
+ adapter: PlatformAdapter
45
+ plugin_dir: Path
46
+ module_name: str
47
+
48
+
49
+ def discover_plugin_dirs(plugins_root: Path) -> List[Path]:
50
+ """扫描插件根目录,返回包含 manifest 的子目录列表。"""
51
+ root = plugins_root.resolve()
52
+ if not root.is_dir():
53
+ return []
54
+
55
+ candidates: List[Path] = []
56
+ for child in sorted(root.iterdir()):
57
+ if not child.is_dir():
58
+ continue
59
+ if child.name.startswith(".") or child.name.startswith("_"):
60
+ continue
61
+ if (child / _MANIFEST_NAME).is_file() and (child / _ENTRY_MODULE).is_file():
62
+ candidates.append(child)
63
+ return candidates
64
+
65
+
66
+ def _topological_sort(manifests: Mapping[str, PluginManifest]) -> List[str]:
67
+ indegree: Dict[str, int] = {pid: 0 for pid in manifests}
68
+ graph: Dict[str, List[str]] = {pid: [] for pid in manifests}
69
+
70
+ for pid, manifest in manifests.items():
71
+ for dep in manifest.dependencies:
72
+ if dep not in manifests:
73
+ raise PluginLoadError(f"插件 {pid} 依赖未找到的插件: {dep}")
74
+ graph[dep].append(pid)
75
+ indegree[pid] += 1
76
+
77
+ queue = deque(sorted(pid for pid, deg in indegree.items() if deg == 0))
78
+ ordered: List[str] = []
79
+ while queue:
80
+ current = queue.popleft()
81
+ ordered.append(current)
82
+ for nxt in graph[current]:
83
+ indegree[nxt] -= 1
84
+ if indegree[nxt] == 0:
85
+ queue.append(nxt)
86
+
87
+ if len(ordered) != len(manifests):
88
+ raise PluginLoadError("插件依赖存在环")
89
+ return ordered
90
+
91
+
92
+ def _load_entry_module(plugin_dir: Path, plugin_id: str) -> Any:
93
+ module_name = f"provider_plugin_{plugin_id.replace('.', '_').replace('-', '_')}"
94
+ entry = plugin_dir / _ENTRY_MODULE
95
+ spec = importlib.util.spec_from_file_location(module_name, entry)
96
+ if spec is None or spec.loader is None:
97
+ raise PluginLoadError(f"无法加载入口模块: {entry}")
98
+
99
+ module = importlib.util.module_from_spec(spec)
100
+ sys.modules[module_name] = module
101
+ spec.loader.exec_module(module)
102
+ return module
103
+
104
+
105
+ def _validate_lifecycle(plugin: ProviderPlugin) -> None:
106
+ cls = type(plugin)
107
+ if cls.on_load is ProviderPlugin.on_load:
108
+ pass
109
+ if cls.on_unload is ProviderPlugin.on_unload:
110
+ pass
111
+ if cls.on_config_update is ProviderPlugin.on_config_update:
112
+ pass
113
+
114
+
115
+ class PluginLoader:
116
+ """扫描 ``plugins/`` 目录并加载 SDK 平台插件。"""
117
+
118
+ def __init__(
119
+ self,
120
+ *,
121
+ host_version: str = "",
122
+ plugin_type_filter: str = "platform",
123
+ ) -> None:
124
+ self._host_version = host_version
125
+ self._plugin_type_filter = plugin_type_filter.strip().lower()
126
+ self._loaded: Dict[str, LoadedPlugin] = {}
127
+ self._failed: Dict[str, str] = {}
128
+
129
+ @property
130
+ def loaded_plugins(self) -> Dict[str, LoadedPlugin]:
131
+ return dict(self._loaded)
132
+
133
+ @property
134
+ def failed_plugins(self) -> Dict[str, str]:
135
+ return dict(self._failed)
136
+
137
+ async def discover_and_load(
138
+ self,
139
+ plugins_root: Path,
140
+ session: aiohttp.ClientSession,
141
+ *,
142
+ get_plugin_config: Optional[Callable[[str], Mapping[str, Any]]] = None,
143
+ get_global_config: Optional[Callable[[], Mapping[str, Any]]] = None,
144
+ whitelist: Optional[Sequence[str]] = None,
145
+ blacklist: Optional[Sequence[str]] = None,
146
+ ) -> List[LoadedPlugin]:
147
+ """发现并加载全部合法插件。"""
148
+ plugin_dirs = discover_plugin_dirs(plugins_root)
149
+ manifests: Dict[str, PluginManifest] = {}
150
+ dir_by_id: Dict[str, Path] = {}
151
+
152
+ for plugin_dir in plugin_dirs:
153
+ try:
154
+ manifest = load_manifest_file(plugin_dir)
155
+ except Exception as exc:
156
+ self._failed[plugin_dir.name] = str(exc)
157
+ logger.error("manifest 解析失败 [%s]: %s", plugin_dir.name, exc)
158
+ continue
159
+
160
+ if self._plugin_type_filter and manifest.plugin_type != self._plugin_type_filter:
161
+ continue
162
+
163
+ manifests[manifest.id] = manifest
164
+ dir_by_id[manifest.id] = plugin_dir
165
+
166
+ wl = set(whitelist) if whitelist else None
167
+ bl = set(blacklist or [])
168
+ ordered_ids = _topological_sort(manifests)
169
+
170
+ loaded: List[LoadedPlugin] = []
171
+ for plugin_id in ordered_ids:
172
+ manifest = manifests[plugin_id]
173
+ adapter_name = plugin_id.rsplit(".", 1)[-1]
174
+
175
+ if wl is not None and adapter_name not in wl and plugin_id not in wl:
176
+ continue
177
+ if adapter_name in bl or plugin_id in bl:
178
+ continue
179
+
180
+ plugin_dir = dir_by_id[plugin_id]
181
+ try:
182
+ record = await self._load_one(
183
+ plugin_dir,
184
+ manifest,
185
+ session,
186
+ get_plugin_config=get_plugin_config,
187
+ get_global_config=get_global_config,
188
+ )
189
+ except Exception as exc:
190
+ self._failed[plugin_id] = str(exc)
191
+ logger.error("插件加载失败 [%s]: %s", plugin_id, exc)
192
+ continue
193
+
194
+ self._loaded[plugin_id] = record
195
+ loaded.append(record)
196
+
197
+ return loaded
198
+
199
+ async def _load_one(
200
+ self,
201
+ plugin_dir: Path,
202
+ manifest: PluginManifest,
203
+ session: aiohttp.ClientSession,
204
+ *,
205
+ get_plugin_config: Optional[Callable[[str], Mapping[str, Any]]] = None,
206
+ get_global_config: Optional[Callable[[], Mapping[str, Any]]] = None,
207
+ ) -> LoadedPlugin:
208
+ module = _load_entry_module(plugin_dir, manifest.id)
209
+ factory = getattr(module, _FACTORY_NAME, None)
210
+ if not callable(factory):
211
+ raise PluginLoadError(f"插件 {manifest.id} 缺少 {_FACTORY_NAME}() 工厂函数")
212
+
213
+ instance = factory()
214
+ if not is_platform_plugin(instance):
215
+ raise PluginLoadError(f"插件 {manifest.id} 必须返回 ProviderPlugin 实例")
216
+
217
+ _validate_lifecycle(instance)
218
+ adapter = get_platform_adapter(instance)
219
+
220
+ if adapter.name != manifest.id.split(".")[-1] and adapter.name != manifest.id:
221
+ logger.warning(
222
+ "插件 %s 的 adapter.name=%s 与 manifest id 不完全一致",
223
+ manifest.id,
224
+ adapter.name,
225
+ )
226
+
227
+ config_lookup = get_plugin_config or (lambda _pid: {})
228
+ services = HostServices(
229
+ session=session,
230
+ plugin_id=manifest.id,
231
+ plugin_dir=str(plugin_dir.resolve()),
232
+ get_plugin_config=lambda: config_lookup(manifest.id),
233
+ get_global_config=get_global_config,
234
+ )
235
+ ctx = create_plugin_context(services)
236
+ instance._set_context(ctx)
237
+
238
+ initial_config = config_lookup(manifest.id)
239
+ if isinstance(initial_config, Mapping):
240
+ instance.set_plugin_config(dict(initial_config))
241
+
242
+ await instance.on_load()
243
+ await adapter.init(session)
244
+
245
+ return LoadedPlugin(
246
+ manifest=manifest,
247
+ plugin=instance,
248
+ adapter=adapter,
249
+ plugin_dir=plugin_dir,
250
+ module_name=module.__name__,
251
+ )
252
+
253
+ async def unload_all(self) -> None:
254
+ """卸载全部已加载插件。"""
255
+ for plugin_id, record in list(self._loaded.items()):
256
+ try:
257
+ await record.adapter.close()
258
+ except Exception as exc:
259
+ logger.warning("关闭适配器失败 [%s]: %s", plugin_id, exc)
260
+ try:
261
+ await record.plugin.on_unload()
262
+ except Exception as exc:
263
+ logger.warning("on_unload 失败 [%s]: %s", plugin_id, exc)
264
+ self._loaded.clear()
@@ -0,0 +1,14 @@
1
+ """类型常量与重载范围定义。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from provider_sdk.types.candidate import ALL_CAPABILITIES
6
+
7
+ CONFIG_RELOAD_SCOPE_SELF = "self"
8
+ CONFIG_RELOAD_SCOPE_GLOBAL = "global"
9
+
10
+ __all__ = [
11
+ "CONFIG_RELOAD_SCOPE_SELF",
12
+ "CONFIG_RELOAD_SCOPE_GLOBAL",
13
+ "ALL_CAPABILITIES",
14
+ ]
@@ -0,0 +1,138 @@
1
+ """网关候选项类型 — 与 Provider Host ``Candidate`` 字段对齐。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import time
7
+ import uuid
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ __all__ = ["ALL_CAPABILITIES", "Candidate", "make_id"]
12
+
13
+ ALL_CAPABILITIES: tuple[str, ...] = (
14
+ "chat",
15
+ "vision",
16
+ "image_gen",
17
+ "image_edit",
18
+ "image_variation",
19
+ "video_gen",
20
+ "audio_gen",
21
+ "audio_in",
22
+ "audio_transcription",
23
+ "audio_translation",
24
+ "embedding",
25
+ "research",
26
+ "thinking",
27
+ "search",
28
+ "code_exec",
29
+ "artifacts",
30
+ "tools",
31
+ "native_tools",
32
+ "upload",
33
+ "continuation",
34
+ "moderation",
35
+ "rerank",
36
+ "batch",
37
+ "fine_tuning",
38
+ "files",
39
+ "assistants",
40
+ "threads",
41
+ "runs",
42
+ "vector_stores",
43
+ "realtime",
44
+ "responses",
45
+ )
46
+
47
+
48
+ def make_id(platform: str, resource_id: str = "") -> str:
49
+ """生成候选项 ID。
50
+
51
+ Args:
52
+ platform: 平台标识名。
53
+ resource_id: 平台资源标识;提供时生成确定性 ID。
54
+
55
+ Returns:
56
+ 格式为 ``{platform}_{hash12}`` 的 ID。
57
+ """
58
+ if resource_id:
59
+ digest = hashlib.sha256(f"{platform}:{resource_id}".encode()).hexdigest()[:12]
60
+ return f"{platform}_{digest}"
61
+ return f"{platform}_{uuid.uuid4().hex[:12]}"
62
+
63
+
64
+ @dataclass
65
+ class Candidate:
66
+ """候选项,包含能力布尔字段与元数据。"""
67
+
68
+ id: str
69
+ platform: str
70
+ resource_id: str
71
+
72
+ chat: bool = False
73
+ vision: bool = False
74
+ tools: bool = False
75
+ native_tools: bool = False
76
+ thinking: bool = False
77
+ search: bool = False
78
+ continuation: bool = False
79
+
80
+ image_gen: bool = False
81
+ image_edit: bool = False
82
+ image_variation: bool = False
83
+ video_gen: bool = False
84
+ audio_gen: bool = False
85
+
86
+ audio_in: bool = False
87
+ audio_transcription: bool = False
88
+ audio_translation: bool = False
89
+
90
+ embedding: bool = False
91
+ rerank: bool = False
92
+
93
+ research: bool = False
94
+ code_exec: bool = False
95
+ artifacts: bool = False
96
+ moderation: bool = False
97
+ responses: bool = False
98
+
99
+ upload: bool = False
100
+ files: bool = False
101
+ vector_stores: bool = False
102
+
103
+ batch: bool = False
104
+ fine_tuning: bool = False
105
+
106
+ assistants: bool = False
107
+ threads: bool = False
108
+ runs: bool = False
109
+
110
+ realtime: bool = False
111
+
112
+ context_length: Optional[int] = None
113
+
114
+ models: List[str] = field(default_factory=list)
115
+ available: bool = True
116
+ busy: bool = False
117
+ cooldown: float = 0.0
118
+ meta: Dict[str, Any] = field(default_factory=dict)
119
+
120
+ def has_capability(self, cap: str) -> bool:
121
+ """检查是否具备指定能力。"""
122
+ return bool(getattr(self, cap, False))
123
+
124
+ def to_model_dict(self, owned_by: str = "") -> Dict[str, Any]:
125
+ """转换为 ``/v1/models`` 条目格式。"""
126
+ caps: Dict[str, bool] = {}
127
+ for cap in ALL_CAPABILITIES:
128
+ if getattr(self, cap, False):
129
+ caps[cap] = True
130
+ result: Dict[str, Any] = {
131
+ "object": "model",
132
+ "created": int(time.time()),
133
+ "owned_by": owned_by or self.platform,
134
+ "capabilities": caps,
135
+ }
136
+ if self.context_length is not None:
137
+ result["context_length"] = self.context_length
138
+ return result
@@ -0,0 +1,108 @@
1
+ """插件 manifest 解析与校验。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Mapping, Optional
10
+
11
+ __all__ = ["PluginManifest", "parse_manifest", "load_manifest_file"]
12
+
13
+ _MANIFEST_FILENAME = "_manifest.json"
14
+ _PLUGIN_ID_RE = re.compile(r"^[a-z0-9][a-z0-9._-]{1,126}[a-z0-9]$")
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class PluginManifest:
19
+ """``_manifest.json`` 强类型视图。"""
20
+
21
+ id: str
22
+ name: str
23
+ version: str
24
+ description: str = ""
25
+ plugin_type: str = "platform"
26
+ author: str = ""
27
+ dependencies: List[str] = field(default_factory=list)
28
+ capabilities: List[str] = field(default_factory=list)
29
+ host_min_version: str = ""
30
+ host_max_version: str = ""
31
+ sdk_min_version: str = "0.1.0"
32
+ sdk_max_version: str = "0.99.99"
33
+ manifest_version: int = 1
34
+ raw: Dict[str, Any] = field(default_factory=dict)
35
+
36
+ @property
37
+ def plugin_dependency_ids(self) -> List[str]:
38
+ """依赖插件 ID 列表(``dependencies`` 字段别名)。"""
39
+ return list(self.dependencies)
40
+
41
+
42
+ def _read_author(value: Any) -> str:
43
+ if isinstance(value, str):
44
+ return value.strip()
45
+ if isinstance(value, Mapping):
46
+ return str(value.get("name") or "").strip()
47
+ return ""
48
+
49
+
50
+ def _read_dependencies(value: Any) -> List[str]:
51
+ if not isinstance(value, list):
52
+ return []
53
+ out: List[str] = []
54
+ for item in value:
55
+ if isinstance(item, str) and item.strip():
56
+ out.append(item.strip())
57
+ elif isinstance(item, Mapping):
58
+ dep_id = str(item.get("id") or item.get("plugin_id") or "").strip()
59
+ if dep_id:
60
+ out.append(dep_id)
61
+ return out
62
+
63
+
64
+ def parse_manifest(data: Mapping[str, Any]) -> PluginManifest:
65
+ """将原始 JSON 映射解析为 :class:`PluginManifest`。
66
+
67
+ Raises:
68
+ ValueError: manifest 缺少必填字段或格式非法。
69
+ """
70
+ plugin_id = str(data.get("id") or "").strip()
71
+ if not plugin_id:
72
+ raise ValueError("manifest 缺少 id")
73
+ if not _PLUGIN_ID_RE.match(plugin_id):
74
+ raise ValueError(f"manifest id 格式非法: {plugin_id}")
75
+
76
+ name = str(data.get("name") or plugin_id).strip()
77
+ version = str(data.get("version") or "0.0.0").strip()
78
+ host_app = data.get("host_application") if isinstance(data.get("host_application"), Mapping) else {}
79
+ sdk_info = data.get("sdk") if isinstance(data.get("sdk"), Mapping) else {}
80
+
81
+ return PluginManifest(
82
+ id=plugin_id,
83
+ name=name,
84
+ version=version,
85
+ description=str(data.get("description") or "").strip(),
86
+ plugin_type=str(data.get("plugin_type") or "platform").strip().lower(),
87
+ author=_read_author(data.get("author")),
88
+ dependencies=_read_dependencies(data.get("dependencies")),
89
+ capabilities=[str(x).strip() for x in (data.get("capabilities") or []) if str(x).strip()],
90
+ host_min_version=str(host_app.get("min_version") or "").strip(),
91
+ host_max_version=str(host_app.get("max_version") or "").strip(),
92
+ sdk_min_version=str(sdk_info.get("min_version") or "0.1.0").strip(),
93
+ sdk_max_version=str(sdk_info.get("max_version") or "0.99.99").strip(),
94
+ manifest_version=int(data.get("manifest_version") or 1),
95
+ raw=dict(data),
96
+ )
97
+
98
+
99
+ def load_manifest_file(plugin_dir: Path) -> PluginManifest:
100
+ """从插件目录读取 ``_manifest.json``。"""
101
+ path = plugin_dir / _MANIFEST_FILENAME
102
+ if not path.is_file():
103
+ raise FileNotFoundError(f"缺少 {_MANIFEST_FILENAME}: {plugin_dir}")
104
+ with path.open("r", encoding="utf-8") as fh:
105
+ data = json.load(fh)
106
+ if not isinstance(data, Mapping):
107
+ raise ValueError(f"manifest 根节点必须是对象: {path}")
108
+ return parse_manifest(data)
@@ -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,17 @@
1
+ provider_sdk/__init__.py,sha256=_tfZdsO_EM_-poHePu_o_91h0DGOqdZuy8fLlS7rG28,1168
2
+ provider_sdk/config.py,sha256=pgi9DeDtjX0iv6MR1eEK5tNUwyv9arTy2n_10acDab4,2490
3
+ provider_sdk/context.py,sha256=BF4uT6qQbnmMMnMC-0RSD7AWlKzC6L9Pyc1CNY9LpUc,2250
4
+ provider_sdk/integrate.py,sha256=-dySX5DnEPfrGUxEW3CCvM58j4cxm3WULekwcoCEZpA,1850
5
+ provider_sdk/platform.py,sha256=rXPbRPorYYLlmOnQ9o3rMzAD-l0CPkQXszuvwZNCriM,4072
6
+ provider_sdk/plugin.py,sha256=tEMum5CN-xHGsnsc6Tx8CvXGssCxauYTmSyWcXLf8IM,5821
7
+ provider_sdk/capabilities/__init__.py,sha256=WCQqMcNOFGnU9eQsPC4rj0QYpy7rUU4uyNz9IkMrgPs,2235
8
+ provider_sdk/runtime/__init__.py,sha256=dWU2zbaNJOIqrEwAjR-KYDr6B3u4UrVWXHF2siG94lA,282
9
+ provider_sdk/runtime/loader.py,sha256=-87KaYYRu2ly8btOJK9uCC6sqhbkB1FO1YZd1AjgzdQ,9088
10
+ provider_sdk/types/__init__.py,sha256=QpX4KxNbqHL54qgqeGdCmFUWLcIWdBl_edAsxcyUcWk,330
11
+ provider_sdk/types/candidate.py,sha256=MWPK4L9gdGEdxkI4Yzk4TZ5-EBn_7okQ5kHuAJd7y0o,3468
12
+ provider_sdk/types/manifest.py,sha256=WEv3ep_YKyxBo7tV1SaWeJgT8Oq56AKLSCP26VVoqYE,3948
13
+ provider_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=hUIaOfCtc8HRKim1ozd7uE6nPGr4fmfOlY-4rK6lwPs,1103
14
+ provider_sdk-0.1.0.dist-info/METADATA,sha256=6viMG9fsnmHgr308bLg8HGbGjpxel3VSCiYmZMuvJrE,3295
15
+ provider_sdk-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
16
+ provider_sdk-0.1.0.dist-info/top_level.txt,sha256=gp3jTnkwS1ISaTgWZLrHyxpiNDiTdcuq-Ze1QXu9ebI,13
17
+ provider_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ provider_sdk