A proxy layer for OpenAI-compatible APIs with pre/post-processing hooks
Project description
API Model Proxy
A lightweight, extensible proxy layer for OpenAI-compatible APIs. Drop it between your application and any OpenAI-compatible backend to intercept, log, modify, or filter requests and responses — without changing your existing client code.
Features
- OpenAI-compatible — works as a drop-in with any
openai.OpenAIclient by settingbase_url - Hook-based — override
_preprocess_requestand_postprocess_responseto customise behaviour - Full API coverage — proxies all inference endpoints (chat, completions, responses, embeddings, audio, images, moderations) with hooks; all other endpoints (models, files, fine-tuning, batches, vector stores, etc.) are forwarded transparently
- Error visibility —
_postprocess_responseis called on both success and error responses, allowing subclasses to inspect or modify errors - FastAPI + uvicorn — async, production-ready server with auto-generated docs at
/docs
Use Cases
| Use case | Approach |
|---|---|
| Request/response logging | Override both hooks to log to stdout, a file, or an external service |
| Model gating / filtering | Raise in _preprocess_request to block disallowed models or prompt patterns |
| Response caching | Cache response dicts in _postprocess_response and serve from _preprocess_request |
| Rate limiting / usage tracking | Track request counts or token usage per user in _postprocess_response |
| Prompt augmentation | Inject system messages or rewrite user messages in _preprocess_request |
| A/B testing | Route requests to different model versions conditionally in _preprocess_request |
Installation
pip install api-model-proxy
Quick Start
1. Deploy the proxy
from openai import OpenAI
from api_model_proxy import APIModelProxy
class LoggingProxy(APIModelProxy):
def _preprocess_request(self, request):
print(f"Request: {request}")
return request
def _postprocess_response(self, response):
print(f"Response: {response}")
return response
client = OpenAI(
api_key="your-api-key",
base_url="https://api.openai.com/v1", # or any OpenAI-compatible endpoint
)
proxy = LoggingProxy(client)
proxy.deploy(host="localhost", port=8000)
2. Point your client at the proxy
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000", api_key="EMPTY")
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello, world!"}]
)
print(response.choices[0].message.content)
No other changes needed — the proxy is fully transparent to the client.
Deployment
Docker
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -e .
CMD ["python", "-c", "
from openai import OpenAI
from api_model_proxy import APIModelProxy
class LoggingProxy(APIModelProxy):
def _preprocess_request(self, request):
print(f'Request: {request}')
return request
def _postprocess_response(self, response):
print(f'Response: {response}')
return response
client = OpenAI(api_key='your-api-key', base_url='https://api.openai.com/v1')
LoggingProxy(client).deploy(host='0.0.0.0', port=8000)
"]
docker-compose
version: "3.9"
services:
proxy:
build: .
ports:
- "8000:8000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY:?required}
Run with:
OPENAI_API_KEY=sk-... docker compose up
Customisation
Subclass APIModelProxy and override either or both hooks:
| Method | Called on | Default behaviour |
|---|---|---|
_preprocess_request(request: dict) -> dict |
Every inference request, before forwarding | No-op (return as-is) |
_postprocess_response(response: dict) -> dict |
Every inference response (success and error) | No-op (return as-is) |
Example: request filtering
class FilterProxy(APIModelProxy):
_BLOCKED = {"gpt-4o", "gpt-4-turbo"}
def _preprocess_request(self, request):
if request.get("model") in self._BLOCKED:
raise ValueError(f"Model {request['model']} is not allowed.")
return request
Example: response caching
import hashlib, json
class CachingProxy(APIModelProxy):
def __init__(self, client):
super().__init__(client)
self._cache = {}
self._last_key = None
def _preprocess_request(self, request):
self._last_key = hashlib.sha256(json.dumps(request, sort_keys=True).encode()).hexdigest()
return request
def _postprocess_response(self, response):
if self._last_key:
self._cache[self._last_key] = response
return response
More examples
See the examples/ directory for ready-to-use proxy implementations:
persistant_logging_proxy.py— logs every request/response to daily JSONL or YAML filescaching_proxy.py— in-memory LRU response cache with configurable TTLfallback_proxy.py— multi-backend circuit-breaker with automatic fallbackrate_limiting_proxy.py— token-bucket rate limiter returning429 Too Many Requests
Endpoints
Inference (hooks fire)
| Method | Path |
|---|---|
| POST | /chat/completions |
| POST | /completions |
| POST | /responses |
| POST | /embeddings |
| POST | /audio/transcriptions |
| POST | /audio/translations |
| POST | /audio/speech |
| POST | /images/generations |
| POST | /images/edits |
| POST | /images/variations |
| POST | /moderations |
All paths are also available with a /v1/ prefix.
Passthrough (no hooks, forwarded verbatim)
All other endpoints — models, files, fine-tuning, batches, vector stores, evals, organisation admin, and any future OpenAI endpoints — are forwarded transparently using the upstream client's base URL and API key.
Streaming
Streaming (stream=True) is not yet supported. Requests with stream=True will receive a 501 Not Implemented response. Streaming support is planned for the next version.
Running Tests
The test suite uses pytest. Install the development dependencies and run:
pip install pytest pytest-mock
pytest tests/ -v
The tests use a mocked OpenAI client so no API key or network access is required.
Project Structure
src/api_model_proxy/
├── __init__.py # exports APIModelProxy
├── proxy.py # APIModelProxy base class
├── server.py # FastAPI app factory
└── routes/
├── chat.py # POST /chat/completions
├── completions.py # POST /completions
├── responses.py # POST /responses
├── embeddings.py # POST /embeddings
├── audio.py # POST /audio/*
├── images.py # POST /images/*
├── moderations.py # POST /moderations
└── passthrough.py # catch-all transparent proxy
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file api_model_proxy-0.1.2.tar.gz.
File metadata
- Download URL: api_model_proxy-0.1.2.tar.gz
- Upload date:
- Size: 16.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f2d0018d60416ea7f351b7c1ba8c4f4309f8b4a1607c1d0c866e416bd825ae6d
|
|
| MD5 |
57c7a5919a0d26c6217d4cf6b3518a5a
|
|
| BLAKE2b-256 |
f40e9c49c08472dfab61ce9c0f2ff955e3b8141e7862b304dd32809c91d80de3
|
Provenance
The following attestation bundles were made for api_model_proxy-0.1.2.tar.gz:
Publisher:
python-publish.yml on panuthept/api-model-proxy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
api_model_proxy-0.1.2.tar.gz -
Subject digest:
f2d0018d60416ea7f351b7c1ba8c4f4309f8b4a1607c1d0c866e416bd825ae6d - Sigstore transparency entry: 1748953115
- Sigstore integration time:
-
Permalink:
panuthept/api-model-proxy@20177f47b94ead9e8a0f5cfdb1605580529733ec -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/panuthept
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@20177f47b94ead9e8a0f5cfdb1605580529733ec -
Trigger Event:
release
-
Statement type:
File details
Details for the file api_model_proxy-0.1.2-py3-none-any.whl.
File metadata
- Download URL: api_model_proxy-0.1.2-py3-none-any.whl
- Upload date:
- Size: 15.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d0fd818790f70d2f52c38efba58a425c8996b9d8e6fbe81ac63716dd46a1d7f3
|
|
| MD5 |
75125fe1bd6e3777f94b2f175f59851b
|
|
| BLAKE2b-256 |
9658a894ff9b9b47e00d700ab43f1c3909054270226de9d9675a7dffbaf8f311
|
Provenance
The following attestation bundles were made for api_model_proxy-0.1.2-py3-none-any.whl:
Publisher:
python-publish.yml on panuthept/api-model-proxy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
api_model_proxy-0.1.2-py3-none-any.whl -
Subject digest:
d0fd818790f70d2f52c38efba58a425c8996b9d8e6fbe81ac63716dd46a1d7f3 - Sigstore transparency entry: 1748953420
- Sigstore integration time:
-
Permalink:
panuthept/api-model-proxy@20177f47b94ead9e8a0f5cfdb1605580529733ec -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/panuthept
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@20177f47b94ead9e8a0f5cfdb1605580529733ec -
Trigger Event:
release
-
Statement type: