localstack-extension-bedrock-mantle 0.1.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.
@@ -0,0 +1,137 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib64/
18
+ parts/
19
+ sdist/
20
+ var/
21
+ wheels/
22
+ pip-wheel-metadata/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+
53
+ # Translations
54
+ *.mo
55
+ *.pot
56
+
57
+ # Django stuff:
58
+ *.log
59
+ local_settings.py
60
+ db.sqlite3
61
+ db.sqlite3-journal
62
+
63
+ # Flask stuff:
64
+ instance/
65
+ .webassets-cache
66
+
67
+ # Scrapy stuff:
68
+ .scrapy
69
+
70
+ # Sphinx documentation
71
+ docs/_build/
72
+
73
+ # PyBuilder
74
+ target/
75
+
76
+ # Jupyter Notebook
77
+ .ipynb_checkpoints
78
+
79
+ # IPython
80
+ profile_default/
81
+ ipython_config.py
82
+
83
+ # pyenv
84
+ .python-version
85
+
86
+ # pipenv
87
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
88
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
89
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
90
+ # install all needed dependencies.
91
+ #Pipfile.lock
92
+
93
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
94
+ __pypackages__/
95
+
96
+ # Celery stuff
97
+ celerybeat-schedule
98
+ celerybeat.pid
99
+
100
+ # SageMath parsed files
101
+ *.sage.py
102
+
103
+ # Environments
104
+ .env
105
+ .venv
106
+ env/
107
+ venv/
108
+ ENV/
109
+ env.bak/
110
+ venv.bak/
111
+
112
+ # Spyder project settings
113
+ .spyderproject
114
+ .spyproject
115
+
116
+ # Rope project settings
117
+ .ropeproject
118
+
119
+ # mkdocs documentation
120
+ /site
121
+
122
+ # mypy
123
+ .mypy_cache/
124
+ .dmypy.json
125
+ dmypy.json
126
+
127
+ # Pyre type checker
128
+ .pyre/
129
+
130
+ # IDE files
131
+ .idea
132
+ .vscode
133
+
134
+ node_modules/
135
+ .DS_Store
136
+
137
+ .wiremock
@@ -0,0 +1,30 @@
1
+ VENV_BIN = python3 -m venv
2
+ VENV_DIR ?= .venv
3
+ VENV_ACTIVATE = $(VENV_DIR)/bin/activate
4
+ VENV_RUN = . $(VENV_ACTIVATE)
5
+
6
+ venv: $(VENV_ACTIVATE)
7
+
8
+ $(VENV_ACTIVATE): pyproject.toml
9
+ test -d .venv || $(VENV_BIN) .venv
10
+ $(VENV_RUN); pip install --upgrade pip
11
+ $(VENV_RUN); pip install -e .[dev]
12
+ touch $(VENV_DIR)/bin/activate
13
+
14
+ clean:
15
+ rm -rf .venv/ build/ dist/ .eggs/ *.egg-info/
16
+
17
+ install: venv
18
+
19
+ dist: venv
20
+ $(VENV_RUN); python -m build
21
+
22
+ lint: venv
23
+ $(VENV_RUN); ruff check .
24
+ $(VENV_RUN); ruff format --check .
25
+
26
+ format: venv
27
+ $(VENV_RUN); ruff check --fix .
28
+ $(VENV_RUN); ruff format .
29
+
30
+ .PHONY: clean dist install venv lint format
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.5
2
+ Name: localstack-extension-bedrock-mantle
3
+ Version: 0.1.0
4
+ Summary: Emulates Bedrock Mantle with Ollama's OpenAI-compatible API
5
+ Project-URL: Homepage, https://github.com/thrau/localstack-extension-bedrock-mantle
6
+ Author-email: Thomas Rausch <thomas@thrau.at>
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: localstack-extensions-utils
9
+ Provides-Extra: dev
10
+ Requires-Dist: boto3; extra == 'dev'
11
+ Requires-Dist: build; extra == 'dev'
12
+ Requires-Dist: hatch; extra == 'dev'
13
+ Requires-Dist: localstack-core>=4.0; extra == 'dev'
14
+ Requires-Dist: ruff; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Bedrock Mantle LocalStack Extension
18
+
19
+ Emulates [Bedrock Mantle](https://github.com/awslabs/bedrock-mantle) by exposing the
20
+ OpenAI-compatible API provided by Ollama through LocalStack endpoints. The extension
21
+ uses LocalStack Bedrock's `Ollama` backend, so Ollama is started and managed by
22
+ LocalStack.
23
+
24
+ ## Install
25
+
26
+ To install the extension, add to your environment
27
+
28
+ ```
29
+ EXTENSION_AUTO_INSTALL=git+https://github.com/thrau/localstack-extension-bedrock-mantle/#egg=localstack-extension-bedrock-mantle
30
+ ```
31
+
32
+ or run
33
+
34
+ ```sh
35
+ localstack extensions install "git+https://github.com/thrau/localstack-extension-bedrock-mantle/#egg=localstack-extension-bedrock-mantle"
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ To connect to AWS bedrock mantle, your OpenAI code might look as simple as this.
41
+
42
+ ```python
43
+ client = OpenAI(
44
+ provider=bedrock(
45
+ endpoint="mantle",
46
+ api_key=None, # Force IAM/SigV4 authentication
47
+ )
48
+ )
49
+ ```
50
+ The extension automatically configures your Lambda containers to connect to localstack.
51
+
52
+ ## How it works
53
+
54
+ Requests to either of these endpoint forms are proxied directly to Ollama:
55
+
56
+ - `http://bedrock-mantle.<region>.localhost.localstack.cloud:4566/v1/...`
57
+ - `http://bedrock-mantle.<region>.api.aws/v1/...`
58
+
59
+ For example, use `https://bedrock-mantle.us-east-1.localhost.localstack.cloud/v1`
60
+ as an OpenAI base URL.
61
+
62
+ On load, the extension appends the following Docker flag to
63
+ `LAMBDA_DOCKER_FLAGS`, making the emulator available to Docker Lambda functions:
64
+
65
+ ```text
66
+ -e AWS_BEDROCK_BASE_URL=http://bedrock-mantle.us-east-1.localhost.localstack.cloud:4566/v1
67
+ ```
68
+
69
+ ## Local development
70
+
71
+ Install development dependencies with `make install`. Run `make lint` to check lint
72
+ rules, import ordering, and formatting with Ruff, or `make format` to apply automatic
73
+ fixes and format the code.
74
+
75
+ Build the source distribution and wheel with `make dist` (using Hatch and the
76
+ Hatchling build backend). Artifacts are written to `dist/`.
77
+
78
+ ```bash
79
+ localstack extensions dev enable .
80
+ EXTENSION_DEV_MODE=1 localstack start
81
+ ```
@@ -0,0 +1,65 @@
1
+ # Bedrock Mantle LocalStack Extension
2
+
3
+ Emulates [Bedrock Mantle](https://github.com/awslabs/bedrock-mantle) by exposing the
4
+ OpenAI-compatible API provided by Ollama through LocalStack endpoints. The extension
5
+ uses LocalStack Bedrock's `Ollama` backend, so Ollama is started and managed by
6
+ LocalStack.
7
+
8
+ ## Install
9
+
10
+ To install the extension, add to your environment
11
+
12
+ ```
13
+ EXTENSION_AUTO_INSTALL=git+https://github.com/thrau/localstack-extension-bedrock-mantle/#egg=localstack-extension-bedrock-mantle
14
+ ```
15
+
16
+ or run
17
+
18
+ ```sh
19
+ localstack extensions install "git+https://github.com/thrau/localstack-extension-bedrock-mantle/#egg=localstack-extension-bedrock-mantle"
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ To connect to AWS bedrock mantle, your OpenAI code might look as simple as this.
25
+
26
+ ```python
27
+ client = OpenAI(
28
+ provider=bedrock(
29
+ endpoint="mantle",
30
+ api_key=None, # Force IAM/SigV4 authentication
31
+ )
32
+ )
33
+ ```
34
+ The extension automatically configures your Lambda containers to connect to localstack.
35
+
36
+ ## How it works
37
+
38
+ Requests to either of these endpoint forms are proxied directly to Ollama:
39
+
40
+ - `http://bedrock-mantle.<region>.localhost.localstack.cloud:4566/v1/...`
41
+ - `http://bedrock-mantle.<region>.api.aws/v1/...`
42
+
43
+ For example, use `https://bedrock-mantle.us-east-1.localhost.localstack.cloud/v1`
44
+ as an OpenAI base URL.
45
+
46
+ On load, the extension appends the following Docker flag to
47
+ `LAMBDA_DOCKER_FLAGS`, making the emulator available to Docker Lambda functions:
48
+
49
+ ```text
50
+ -e AWS_BEDROCK_BASE_URL=http://bedrock-mantle.us-east-1.localhost.localstack.cloud:4566/v1
51
+ ```
52
+
53
+ ## Local development
54
+
55
+ Install development dependencies with `make install`. Run `make lint` to check lint
56
+ rules, import ordering, and formatting with Ruff, or `make format` to apply automatic
57
+ fixes and format the code.
58
+
59
+ Build the source distribution and wheel with `make dist` (using Hatch and the
60
+ Hatchling build backend). Artifacts are written to `dist/`.
61
+
62
+ ```bash
63
+ localstack extensions dev enable .
64
+ EXTENSION_DEV_MODE=1 localstack start
65
+ ```
@@ -0,0 +1,3 @@
1
+ """Bedrock Mantle emulator extension for LocalStack."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,130 @@
1
+ """Expose Ollama's OpenAI-compatible API through Bedrock Mantle endpoints."""
2
+
3
+ import logging
4
+ import os
5
+ from threading import Lock
6
+ from typing import TYPE_CHECKING
7
+
8
+ import requests
9
+ from localstack import config
10
+ from localstack.extensions.api import Extension, http
11
+ from localstack.pro.core import config as pro_config
12
+
13
+ if TYPE_CHECKING:
14
+ from localstack.pro.core.services.bedrock.backends import Ollama
15
+
16
+ LOG = logging.getLogger(__name__)
17
+
18
+
19
+ class OllamaProxyHandler(http.ProxyHandler):
20
+ """Ensure models requested through the OpenAI API are available before forwarding."""
21
+
22
+ def __init__(self, ollama: "Ollama"):
23
+ super().__init__(forward_base_url=f"http://{ollama.host}:{ollama.port}")
24
+ self.ollama = ollama
25
+ self._pull_lock = Lock()
26
+
27
+ def __call__(self, request: http.Request, **kwargs) -> http.Response:
28
+ if request.method == "POST" and request.path.startswith("/v1/"):
29
+ # Werkzeug caches the body, so the proxy can still forward it unchanged.
30
+ payload = request.get_json(silent=True)
31
+ model = payload.get("model") if isinstance(payload, dict) else None
32
+ if isinstance(model, str) and model.strip():
33
+ self._ensure_model(model)
34
+ return super().__call__(request, **kwargs)
35
+
36
+ def _ensure_model(self, model: str):
37
+ # Serialize the check and pull so concurrent requests do not download the same
38
+ # model twice. Query disk inventory (/tags), not the loaded models (/ps).
39
+ with self._pull_lock:
40
+ response = requests.get(f"{self.ollama.api_url}/tags", timeout=10)
41
+ try:
42
+ response.raise_for_status()
43
+ installed = {entry["name"] for entry in response.json().get("models", [])}
44
+ finally:
45
+ response.close()
46
+
47
+ # Ollama defaults untagged names to :latest (registry hosts may contain ':').
48
+ canonical_model = model if ":" in model.rsplit("/", 1)[-1] else f"{model}:latest"
49
+ if model not in installed and canonical_model not in installed:
50
+ LOG.info("Pulling Ollama model %s for Bedrock Mantle", model)
51
+ self.ollama.pull_model(model_id=model)
52
+
53
+
54
+ class BedrockMantleExtension(Extension):
55
+ """Run Ollama and proxy Bedrock Mantle OpenAI API requests to it."""
56
+
57
+ name = "bedrock-mantle"
58
+
59
+ hostname_prefix = "bedrock-mantle."
60
+ """Used for serving through a host rule."""
61
+
62
+ region = "us-east-1"
63
+ """Default region segment used in the Mantle hostname."""
64
+
65
+ def __init__(self):
66
+ self.ollama: "Ollama | None" = None
67
+
68
+ @property
69
+ def lambda_base_url(self) -> str:
70
+ return f"http://{self.hostname_prefix}{self.region}.{config.LOCALSTACK_HOST}/v1"
71
+
72
+ @property
73
+ def lambda_docker_flag(self) -> str:
74
+ return f"-e AWS_BEDROCK_BASE_URL={self.lambda_base_url}"
75
+
76
+ def on_extension_load(self):
77
+ # TODO: logging should be configured automatically for extensions
78
+ if config.DEBUG:
79
+ level = logging.DEBUG
80
+ else:
81
+ level = logging.INFO
82
+ logging.getLogger("localstack_bedrock_mantle").setLevel(level=level)
83
+
84
+ self._configure_lambda_docker_flags()
85
+
86
+ def on_platform_start(self):
87
+ # Import lazily so the extension can be discovered before LocalStack Pro modules are loaded.
88
+ from localstack.pro.core.services.bedrock.backends import Ollama
89
+
90
+ self.ollama = Ollama()
91
+ LOG.info("starting Ollama for the Bedrock Mantle emulator")
92
+ self.ollama.start()
93
+
94
+ def update_gateway_routes(self, router: http.Router[http.RouteHandler]):
95
+ if not self.ollama:
96
+ raise RuntimeError("Ollama must be started before registering Bedrock Mantle routes")
97
+
98
+ # Ollama exposes its OpenAI-compatible API at /v1. Proxy from the server root so
99
+ # requests such as /v1/chat/completions retain their complete path.
100
+ endpoint = OllamaProxyHandler(self.ollama)
101
+
102
+ # hostname aliases, e.g. bedrock-mantle.<region>.<host>. the wildcard converters make
103
+ # the emulator reachable for any region and any host (e.g. localhost.localstack.cloud,
104
+ # api.aws, ...) without enumerating hosts.
105
+ hostname = f"{self.hostname_prefix}<region>.<host>"
106
+ router.add("/", host=hostname, endpoint=endpoint)
107
+ router.add("/<path:path>", host=hostname, endpoint=endpoint)
108
+
109
+ def on_platform_ready(self):
110
+ hostname_edge_url = f"{config.LOCALSTACK_HOST}"
111
+ url = f"http://{self.hostname_prefix}{hostname_edge_url}/v1"
112
+ LOG.info("serving bedrock mantle emulator on host: %s", url)
113
+
114
+ def on_platform_shutdown(self):
115
+ if self.ollama:
116
+ self.ollama.shutdown()
117
+ self.ollama = None
118
+
119
+ def _configure_lambda_docker_flags(self):
120
+ """Make the Mantle endpoint available as AWS_BEDROCK_BASE_URL in Lambda containers."""
121
+ flags = pro_config.LAMBDA_DOCKER_FLAGS
122
+ if self.lambda_docker_flag not in flags:
123
+ flags = " ".join(filter(None, (flags, self.lambda_docker_flag)))
124
+
125
+ # LocalStack reads the module setting when creating Lambda containers; retain the
126
+ # environment value too for components that consult it after extension loading.
127
+ pro_config.LAMBDA_DOCKER_FLAGS = flags
128
+ os.environ["LAMBDA_DOCKER_FLAGS"] = flags
129
+
130
+ LOG.info("updated LAMBDA_DOCKER_FLAGS: %s", pro_config.LAMBDA_DOCKER_FLAGS)
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "localstack-extension-bedrock-mantle"
7
+ version = "0.1.0"
8
+ description = "Emulates Bedrock Mantle with Ollama's OpenAI-compatible API"
9
+ readme = { file = "README.md", content-type = "text/markdown" }
10
+ requires-python = ">=3.12"
11
+ authors = [{ name = "Thomas Rausch", email = "thomas@thrau.at" }]
12
+ dependencies = [
13
+ "localstack-extensions-utils",
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/thrau/localstack-extension-bedrock-mantle"
18
+
19
+ [project.optional-dependencies]
20
+ dev = ["localstack-core>=4.0", "boto3", "ruff", "build", "hatch"]
21
+
22
+ [project.entry-points."localstack.extensions"]
23
+ bedrock-mantle = "localstack_bedrock_mantle.extension:BedrockMantleExtension"
24
+
25
+ [tool.hatch.build.targets.sdist]
26
+ include = ["/localstack_bedrock_mantle", "/tests", "/Makefile", "/README.md", "/pyproject.toml"]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["localstack_bedrock_mantle"]
30
+
31
+ [tool.ruff]
32
+ line-length = 100
33
+ target-version = "py312"
34
+ extend-exclude = [".venv*", "venv*"]
35
+
36
+ [tool.ruff.lint]
37
+ select = ["B", "C90", "E", "F", "I", "W", "T10"]
38
+ ignore = ["E501", "F403"]