mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-03 22:32:27 +08:00
feat(plugins): consolidate official plugins with independent releases (LANG-180) (#2797)
Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
parent
d099033d79
commit
3a77306c3c
@@ -0,0 +1,167 @@
|
||||
"""Two installations must not share one OpenAI credential or endpoint.
|
||||
|
||||
Pre-fix failure this test pins: ``AIImagePlugin.initialize()`` cached a single
|
||||
``AsyncOpenAI`` built from whichever config the worker started with, and
|
||||
``Draw._execute`` called ``self.plugin.openai_client``. With that code the second
|
||||
installation's ``!draw`` was sent with installation A's API key to installation
|
||||
A's base URL, and ``assert not hasattr(plugin, "openai_client")`` fails.
|
||||
|
||||
Both installations are driven through the *same* plugin object and the *same*
|
||||
``Draw`` component object under the real SDK ``bind_invocation`` /
|
||||
``InstallationBinding`` task-local scope; only the per-binding config differs.
|
||||
``sys.modules["openai"]`` is replaced with a recorder so no network or real SDK is
|
||||
involved (the ``openai`` package is not installed for these local runs).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from langbot_plugin.api.proxies.invocation import bind_invocation
|
||||
from langbot_plugin.entities.io.context import InstallationBinding
|
||||
|
||||
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PLUGIN_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PLUGIN_ROOT))
|
||||
|
||||
from components.commands.draw import Draw # noqa: E402
|
||||
from main import AIImagePlugin # noqa: E402
|
||||
|
||||
CONFIG_A = {
|
||||
"openai_api_key": "key-a",
|
||||
"api_base_url": "https://a.example/v1",
|
||||
"model_name": "model-a",
|
||||
"image_size": "768x1280",
|
||||
}
|
||||
CONFIG_B = {
|
||||
"openai_api_key": "key-b",
|
||||
"api_base_url": "https://b.example/v1",
|
||||
"model_name": "model-b",
|
||||
"image_size": "1280x768",
|
||||
}
|
||||
|
||||
|
||||
def _binding(installation: str) -> InstallationBinding:
|
||||
return InstallationBinding(
|
||||
instance_uuid="instance-1",
|
||||
workspace_uuid=f"workspace-{installation}",
|
||||
installation_uuid=installation,
|
||||
runtime_revision=1,
|
||||
artifact_digest="a" * 64,
|
||||
)
|
||||
|
||||
|
||||
def _ctx(params: list[str]) -> SimpleNamespace:
|
||||
return SimpleNamespace(params=params)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_openai(monkeypatch):
|
||||
"""Record every AsyncOpenAI construction, generate call, and close."""
|
||||
|
||||
created: list[dict] = []
|
||||
|
||||
class _Images:
|
||||
def __init__(self, entry: dict) -> None:
|
||||
self._entry = entry
|
||||
|
||||
async def generate(self, **kwargs):
|
||||
self._entry["generate"] = kwargs
|
||||
return SimpleNamespace(
|
||||
data=[
|
||||
SimpleNamespace(
|
||||
url=f"https://img.test/{self._entry['api_key']}.png"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
class _AsyncOpenAI:
|
||||
def __init__(self, *, api_key: str, base_url: str) -> None:
|
||||
entry = {"api_key": api_key, "base_url": base_url, "closed": False}
|
||||
created.append(entry)
|
||||
self.entry = entry
|
||||
self.images = _Images(entry)
|
||||
|
||||
async def close(self) -> None:
|
||||
self.entry["closed"] = True
|
||||
|
||||
module = types.ModuleType("openai")
|
||||
module.AsyncOpenAI = _AsyncOpenAI
|
||||
monkeypatch.setitem(sys.modules, "openai", module)
|
||||
return created
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_each_installation_uses_its_own_key_base_url_model_and_size(
|
||||
fake_openai,
|
||||
) -> None:
|
||||
handler = SimpleNamespace()
|
||||
plugin = AIImagePlugin()
|
||||
plugin.plugin_runtime_handler = handler
|
||||
draw = Draw()
|
||||
draw.plugin = plugin
|
||||
|
||||
with bind_invocation(handler, config=CONFIG_A, binding=_binding("installation-a")):
|
||||
results_a = [item async for item in draw._execute(_ctx(["a", "cat"]))]
|
||||
with bind_invocation(handler, config=CONFIG_B, binding=_binding("installation-b")):
|
||||
results_b = [item async for item in draw._execute(_ctx(["b", "dog"]))]
|
||||
|
||||
assert [(entry["api_key"], entry["base_url"]) for entry in fake_openai] == [
|
||||
("key-a", "https://a.example/v1"),
|
||||
("key-b", "https://b.example/v1"),
|
||||
]
|
||||
assert fake_openai[0]["generate"] == {
|
||||
"model": "model-a",
|
||||
"prompt": "a cat",
|
||||
"size": "768x1280",
|
||||
"n": 1,
|
||||
}
|
||||
assert fake_openai[1]["generate"] == {
|
||||
"model": "model-b",
|
||||
"prompt": "b dog",
|
||||
"size": "1280x768",
|
||||
"n": 1,
|
||||
}
|
||||
assert results_a[-1].image_url == "https://img.test/key-a.png"
|
||||
assert results_b[-1].image_url == "https://img.test/key-b.png"
|
||||
assert [entry["closed"] for entry in fake_openai] == [True, True]
|
||||
|
||||
# Nothing on the shared object retains a single tenant's credential.
|
||||
assert not hasattr(plugin, "openai_client")
|
||||
assert "key-a" not in repr(vars(plugin))
|
||||
assert "key-b" not in repr(vars(plugin))
|
||||
assert "a.example" not in repr(vars(plugin))
|
||||
assert "b.example" not in repr(vars(plugin))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_key_reports_without_building_a_client(fake_openai) -> None:
|
||||
handler = SimpleNamespace()
|
||||
plugin = AIImagePlugin()
|
||||
plugin.plugin_runtime_handler = handler
|
||||
draw = Draw()
|
||||
draw.plugin = plugin
|
||||
|
||||
config = {"openai_api_key": "", "api_base_url": "https://a.example/v1"}
|
||||
with bind_invocation(handler, config=config, binding=_binding("installation-a")):
|
||||
results = [item async for item in draw._execute(_ctx(["a", "cat"]))]
|
||||
|
||||
assert fake_openai == []
|
||||
assert "API Key" in results[-1].text
|
||||
assert results[-1].image_url is None
|
||||
|
||||
|
||||
def test_dedicated_config_without_an_invocation_is_still_used(fake_openai) -> None:
|
||||
# Dedicated workers attach the legacy config and call the same code path.
|
||||
plugin = AIImagePlugin()
|
||||
plugin.plugin_runtime_handler = SimpleNamespace()
|
||||
plugin.config = {"openai_api_key": "dedicated-key", "api_base_url": "https://d.example/v1"}
|
||||
|
||||
client = plugin.create_client()
|
||||
|
||||
assert client.entry["api_key"] == "dedicated-key"
|
||||
assert client.entry["base_url"] == "https://d.example/v1"
|
||||
Reference in New Issue
Block a user