polyllm-core 0.2.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyllm-core
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: PolyLLM Python core: unified protocol, registry, and parameter policy.
5
5
  Author: PolyLLM contributors
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "polyllm-core"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "PolyLLM Python core: unified protocol, registry, and parameter policy."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import replace
3
4
  from typing import Any, AsyncIterator, Mapping
4
5
 
5
6
  from .config import resolve_provider_runtime
@@ -148,7 +149,8 @@ def create_llm(config: ClientConfig | Mapping[str, Any] | None = None, **kwargs:
148
149
  if not isinstance(config, ClientConfig):
149
150
  raise PolyLLMError("INVALID_CONFIG", "create_llm 配置必须是 ClientConfig 或 dict")
150
151
  config = replace_provider_configs(config)
151
- resolve_runtime_configs(config)
152
+ resolved = resolve_runtime_configs(config)
153
+ config = replace(config, providers=resolved)
152
154
  return PolyLLM(config)
153
155
 
154
156
 
@@ -162,8 +164,6 @@ def _merge_request(
162
164
 
163
165
 
164
166
  def replace_provider_configs(config: ClientConfig) -> ClientConfig:
165
- from dataclasses import replace
166
-
167
167
  providers = {
168
168
  provider: value if isinstance(value, ProviderConfig) else ProviderConfig(**value)
169
169
  for provider, value in config.providers.items()
@@ -226,8 +226,15 @@ def normalize_tool_call(value: Any) -> UnifiedToolCall:
226
226
 
227
227
 
228
228
  def resolve_runtime_configs(config: ClientConfig) -> dict[str, ProviderConfig]:
229
+ plugins_by_provider = {plugin.provider: plugin for plugin in config.plugins}
229
230
  resolved: dict[str, ProviderConfig] = {}
230
231
  for provider, provider_config in config.providers.items():
232
+ plugin = plugins_by_provider.get(provider)
233
+ if plugin is not None and plugin.allow_missing_api_key and not (
234
+ provider_config.api_key or provider_config.api_key_env
235
+ ):
236
+ resolved[provider] = provider_config
237
+ continue
231
238
  runtime = resolve_provider_runtime(provider, provider_config)
232
239
  resolved[provider] = ProviderConfig(
233
240
  api_key=runtime.api_key,
@@ -151,6 +151,9 @@ class ChatContext:
151
151
  class LLMPlugin(ABC):
152
152
  provider: str
153
153
  models: tuple[ModelSpec, ...]
154
+ #: 插件声明"无 API Key 也能工作"(例如本地服务,如 Ollama)。
155
+ #: 设置为 True 时,`create_llm` 在构造阶段不为该 provider 强制校验 API Key。
156
+ allow_missing_api_key: bool = False
154
157
 
155
158
  @abstractmethod
156
159
  async def chat(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyllm-core
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: PolyLLM Python core: unified protocol, registry, and parameter policy.
5
5
  Author: PolyLLM contributors
6
6
  License-Expression: MIT
@@ -142,6 +142,27 @@ class CoreTests(unittest.TestCase):
142
142
  response = asyncio.run(client.chat("test:mini", messages=[{"role": "user", "content": "hi"}]))
143
143
  self.assertEqual(response.model, "test-model")
144
144
 
145
+ def test_runtime_resolved_at_construction_not_request_time(self):
146
+ import os
147
+ os.environ["POLYLLM_TEST_KEY"] = "from-env"
148
+ client = create_llm(
149
+ plugins=[TestPlugin()],
150
+ providers={"test": {"api_key_env": "POLYLLM_TEST_KEY"}},
151
+ )
152
+ os.environ.pop("POLYLLM_TEST_KEY")
153
+ # 构造时已把环境变量解析写回 provider config,请求时不再读环境变量
154
+ self.assertEqual(client._config.providers["test"].api_key, "from-env")
155
+ captured = {}
156
+
157
+ class CapturingPlugin(TestPlugin):
158
+ async def chat(self, request, config, context):
159
+ captured["api_key"] = config.api_key
160
+ return await super().chat(request, config, context)
161
+
162
+ client3 = create_llm(plugins=[CapturingPlugin()], providers={"test": {"api_key": "k"}})
163
+ asyncio.run(client3.chat("test-model", messages=[{"role": "user", "content": "hi"}]))
164
+ self.assertEqual(captured["api_key"], "k")
165
+
145
166
  def test_unlisted_model_resolution(self):
146
167
  client = create_llm(
147
168
  plugins=[TestPlugin()],
File without changes
File without changes
File without changes