Cross-Plugin Access
FrameX allows a plugin to call APIs exposed by other plugins. This is useful for composition, reuse, and gradual migration across teams.
There are two steps to make a cross-plugin call:
- Declare dependencies in
PluginMetadata.required_remote_apis. - Invoke the remote API with
self._call_remote_api(...)inside your plugin class.
1) Declare Required Remote APIs
Always declare what you will call in your plugin’s metadata. For example:
__plugin_meta__ = PluginMetadata(
name="my-plugin",
version="0.1.0",
description="Demonstrates cross-plugin access",
author="touale",
url="http://example.local/FrameX",
required_remote_apis=[
"/api/v1/echo", # HTTP (basic types)
"/api/v1/echo_model", # HTTP (BaseModel payload)
"/api/v1/echo_stream", # HTTP streaming
"echo.EchoPlugin.confess", # FUNC call
],
)
2) Call Remote APIs with self._call_remote_api(...)
Use self._call_remote_api(api_name, **kwargs) inside your plugin. The api_name is either:
- The HTTP path (e.g., "/api/v1/echo"), or
- The FUNC name (e.g., "echo.EchoPlugin.confess").
Arguments are passed as keyword arguments.
- Primitive types (str/int/float/bool) → pass directly.
- Pydantic models → pass a dict (e.g., model.model_dump() or a manually constructed dict).
2.1 HTTP (Basic Types)
Remote provider:
@on_request("/echo", methods=["GET"])
async def echo(self, message: str) -> str:
return message
Caller usage:
result = await self._call_remote_api("/api/v1/echo", message=message)
2.2 HTTP (BaseModel Payload)
Remote provider:
class EchoModel(BaseModel):
id: int
name: str
@on_request("/echo_model", methods=["POST"])
async def echo_model(self, message: str, model: EchoModel) -> str:
return f"{message},{model.model_dump()}"
Caller usage:
res = await self._call_remote_api(
"/api/v1/echo_model",
message=message,
model={"id": 1, "name": "Genshin Impact"} # BaseModel as dict
)
2.3 HTTP Streaming
Remote provider:
@on_request("/api/v1/echo_stream", methods=["GET"], stream=True)
async def echo_stream(self, message: str) -> AsyncGenerator[str, None]:
for ch in f"Streaming fun, {message}":
yield make_stream_event(StreamEnventType.MESSAGE_CHUNK, ch)
await asyncio.sleep(0.1)
yield make_stream_event(StreamEnventType.FINISH)
Caller usage:
stream = await self._call_remote_api("/api/v1/echo_stream", message=message)
stream_text = "".join([extract_content(evt) for evt in stream if "message_chunk" in evt])
2.4 FUNC Calls
@on_request(call_type=ApiType.FUNC)
async def confess(self, message: str) -> str:
return f"Got your message: {message}"
Caller usage:
res = await self._call_remote_api("echo.EchoPlugin.confess", message=message)
3) How to Find api_name
An easy way to discover all available APIs is to check the startup logs, which list detected plugins and endpoints. Example:
09-04 15:10:54 [SUCCESS] framex.plugin.manage | Succeeded to load plugin "echo" from framex.plugins.echo
09-04 15:10:54 [INFO] framex | Start initializing all DeploymentHandle...
09-04 15:10:54 [SUCCESS] framex.plugin.manage | Found plugin HTTP API "/api/v1/echo" from plugin(hello_world)
09-04 15:10:54 [SUCCESS] framex.plugin.manage | Found plugin HTTP API "/api/v1/echo_stream" from plugin(hello_world)
09-04 15:10:54 [SUCCESS] framex.plugin.manage | Found plugin FUNC API "echo.EchoPlugin.confess" from plugin(echo)
4) End-to-End Example
import time
from typing import Any
from framex.consts import VERSION
from framex.plugin import BasePlugin, PluginMetadata, on_register, on_request, remote
__plugin_meta__ = PluginMetadata(
name="invoker",
version=VERSION,
description="原神会调用远程方法哟",
author="原神",
url="https://github.com/touale/FrameX-kit",
required_remote_apis=[
"/api/v1/echo",
"echo.EchoPlugin.confess",
"/api/v1/echo_stream",
"/api/v1/echo_model",
],
)
@on_register()
class InvokerPlugin(BasePlugin):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
@on_request("/evoke_echo", methods=["GET"])
async def evoke(self, message: str) -> list[Any]:
def extract_content(chunk: str) -> str:
return chunk.split('"content": "')[-1].split('"')[0]
echo = await self._call_remote_api("/api/v1/echo", message=message)
stream = await self._call_remote_api("/api/v1/echo_stream", message=message)
stream_text = "".join([extract_content(c) for c in stream if "message_chunk" in c])
confess = await self._call_remote_api("echo.EchoPlugin.confess", message=message)
echo_model = await self._call_remote_api(
"/api/v1/echo_model", message=message, model={"id": 1, "name": "原神"}
)
return [echo, stream_text, confess, echo_model]