Build modular Python services with plug-and-play plugins, clear team boundaries, and transparent API integration.
FrameX is a plugin-first Python framework for teams that need service decomposition, multi-team parallel development, private implementation boundaries, and one consistent service surface across local plugins and upstream APIs.
FrameX is most useful when multiple teams need to ship capabilities in parallel, call each other through stable service interfaces, and keep implementation details private so each team can work without understanding or depending on other teams' codebases.
Use it when you need to:
- build service capabilities as plug-and-play modules
- let multiple engineers or teams ship in parallel with clearer ownership boundaries
- split a growing service into independently evolving capability units
- call other teams' capabilities without depending on their codebases
- expose local plugins and upstream APIs behind one consistent service surface
- integrate third-party or internal HTTP services with minimal client-side changes
- start with simple local execution and scale to Ray when needed
- keep the system extensible as capabilities, teams, and traffic grow
FrameX is built around a few core ideas:
Plugin: a capability package with its own code, metadata, and API surface@on_register(): registers a plugin class as a runtime unit@on_request(...): exposes plugin methods as HTTP APIs, internal callable APIs, or bothrequired_remote_apis: declares which other plugin or HTTP APIs a plugin depends oncall_plugin_api(...): lets one capability call another through a stable service interface@remote(): keeps the same call style across local execution and Ray executionproxyplugin: makes upstream OpenAPI services look like part of the same service surface
Plain FastAPI is a good choice for a single cohesive application. FrameX is better when the real problem is not route handling, but service decomposition, team boundaries, and cross-service integration.
Compared with plain FastAPI, FrameX gives you:
- plugin boundaries for clearer ownership between capabilities and teams
- a better development model for plug-and-play modules and parallel delivery
- one consistent surface for local capabilities and upstream HTTP services
- internal callable APIs in addition to normal HTTP routes
- explicit dependency declarations between capabilities
- the ability to start locally and move to Ray-backed execution without rewriting plugin code
If you only need a small application with a stable route surface and one codebase, plain FastAPI is usually simpler.
- plug-and-play development for modular service capabilities
- clearer boundaries for multi-person and multi-team development
- one consistent surface for local plugins and upstream HTTP APIs
- decorator-based registration for HTTP, internal, and streaming APIs
- local execution by default, with an optional path to Ray Serve
- built-in proxying, auth controls, and flexible configuration sources
Base package:
pip install framex-kitWith Ray Serve support:
pip install "framex-kit[ray]"Requirements:
- Python
>=3.11
Create foo.py:
from typing import Any
from pydantic import BaseModel
from framex.consts import VERSION
from framex.plugin import BasePlugin, PluginMetadata, on_register, on_request
__plugin_meta__ = PluginMetadata(
name="foo",
version=VERSION,
description="A minimal example plugin",
author="you",
url="https://github.com/touale/FrameX-kit",
)
class EchoBody(BaseModel):
text: str
@on_register()
class FooPlugin(BasePlugin):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
@on_request("/foo", methods=["GET"])
async def echo(self, message: str) -> str:
return f"foo: {message}"
@on_request("/foo_model", methods=["POST"])
async def echo_model(self, model: EchoBody) -> dict[str, str]:
return {"text": model.text}Run it:
PYTHONPATH=. framex run --load-plugins fooCall it:
curl "http://127.0.0.1:8080/api/v1/foo?message=hello"Open docs:
http://127.0.0.1:8080/docshttp://127.0.0.1:8080/redochttp://127.0.0.1:8080/api/v1/openapi.json
You can also start with the built-in example plugin:
framex run --load-builtin-plugins echoFrameX exposes a framex CLI:
framex run --host 0.0.0.0 --port 8080 --load-builtin-plugins echoMain options:
--host--port--dashboard-host--dashboard-port--num-cpus--load-plugins--load-builtin-plugins--use-ray/--no-use-ray--enable-proxy/--no-enable-proxy
Important:
--load-pluginsand--load-builtin-pluginsare repeatable options- they are not comma-separated lists
Example:
framex run \
--load-builtin-plugins echo \
--load-plugins foo \
--load-plugins your_project.plugins.barEach plugin module typically defines __plugin_meta__:
__plugin_meta__ = PluginMetadata(
name="demo",
version=VERSION,
description="demo plugin",
author="you",
url="https://example.com",
required_remote_apis=["/api/v1/echo", "echo.EchoPlugin.confess"],
)required_remote_apis can contain:
- HTTP paths such as
/api/v1/echo - internal function APIs such as
echo.EchoPlugin.confess
Use @on_request(...) to expose plugin methods.
Typical modes:
- HTTP API: provide a route path
- function API: use
call_type=ApiType.FUNC - both: use
call_type=ApiType.ALL
Current implementation notes:
- a handler may declare at most one
BaseModelparameter stream=Trueproduces a streaming endpointraw_response=Truebypasses the default response wrapper
Use call_plugin_api(...) to call another registered plugin API:
from framex import call_plugin_api
result = await call_plugin_api("/api/v1/echo", message="hello")FrameX resolves the target API from required_remote_apis. If proxy mode is enabled, unresolved HTTP paths can fall back to the built-in proxy plugin. Inside a plugin class, you can also use the convenience wrapper around the same mechanism.
FrameX provides @remote() for functions, instance methods, and class methods.
- in local mode, async functions are awaited directly and sync functions run in a thread pool
- in Ray mode, calls are wrapped with
ray.remote(...)
from framex.plugin import remote
@remote()
def heavy_job(x: int) -> int:
return x * 2
result = await heavy_job.remote(21)FrameX settings are loaded from:
- environment variables
.env.env.prodconfig.toml[tool.framex]inpyproject.toml
CLI options override the in-memory settings before startup.
Minimal config.toml:
load_builtin_plugins = ["echo"]
load_plugins = ["your_project.plugins.foo"]
[server]
host = "127.0.0.1"
port = 8080
use_ray = false
enable_proxy = false
[plugins.foo]
debug = trueCommon settings:
server.host,server.portserver.use_rayserver.enable_proxyload_builtin_pluginsload_pluginsplugins.<plugin_name>auth.rules
Nested environment variables are supported, for example:
export SERVER__PORT=9000
export SERVER__ENABLE_PROXY=trueFrameX includes a built-in proxy plugin for bridging external HTTP services.
To enable it:
- load the built-in
proxyplugin - set
server.enable_proxy = true - configure upstream service URLs in
plugins.proxy
Example:
load_builtin_plugins = ["proxy"]
[server]
enable_proxy = true
[plugins.proxy]
proxy_urls = ["http://127.0.0.1:9000"]
force_stream_apis = ["/api/v1/chat/stream"]
white_list = ["/*"]
timeout = 600The current implementation reads the upstream /api/v1/openapi.json document and dynamically creates local forwarding routes. It supports:
- query parameters
- JSON request bodies
multipart/form-data- file upload fields
- forced streaming APIs
The built-in example plugin exposes:
GET /api/v1/echoPOST /api/v1/echo_modelGET /api/v1/echo_stream- function API
echo.EchoPlugin.confess
The built-in system proxy plugin is used to:
- register proxy routes from remote OpenAPI specs
- forward unresolved HTTP APIs
- register and call proxy functions
A few implementation details are important for adopters:
- docs are served at
/docsand/redoc - OpenAPI is served at
/api/v1/openapi.json - health endpoints include
/healthand/ping - non-streaming API responses are wrapped into a common JSON envelope unless
raw_response=Trueis used - if
auth.oauthis configured, docs and OpenAPI access are protected through the auth flow
Default wrapped response shape:
{
"status": 200,
"message": "success",
"timestamp": "2026-01-01 12:00:00",
"data": {}
}The src/framex package is organized into a small set of layers:
cli.py: command-line entrypointconfig.py: settings models and source precedenceplugin/: decorators, plugin loading, registration, dependency resolutionadapter/: local and Ray runtime adaptersdriver/: FastAPI application, auth, ingress, middlewareplugins/: built-in plugins such asechoandproxy
FrameX fits well when you want to:
- split a service into independently maintained capability modules
- keep a FastAPI-friendly programming model while adding a plugin boundary
- reuse the same plugin code in local and Ray-based deployments
- gradually bridge legacy HTTP services into a unified API surface
FrameX is usable today, but the project should be treated as an actively evolving framework rather than a frozen platform API.
If you plan to adopt it in production, review the current implementation details and test the behaviors you depend on, especially around proxying, response wrapping, and auth integration.
Contributions are welcome.
A good contribution path is:
- open an issue for bugs, API gaps, or design discussion
- keep pull requests focused and small
- include tests for behavior changes when possible
- update documentation when the public behavior changes
This project is licensed under MIT. See LICENSE.