This document provides instructions for AI coding agents working on the ferrolabsai Python SDK — the official client library for Ferro Labs AI Gateway.
ferrolabsai is a drop-in replacement for the OpenAI Python SDK that routes LLM requests through the Ferro Labs AI Gateway to 30 providers and 2,500+ models. The SDK exposes an OpenAI-compatible surface for chat completions, embeddings, images, the Responses API, moderations, rerank, and the model catalog, plus gateway-specific surface: health probes, /v1/capabilities, and the /admin/* management API.
- Package name:
ferrolabsai - Version:
pyproject.toml[project].versionandferrolabsai/_version.py(kept equal; a test asserts it) - Compatibility:
ferrolabsai 0.3.x↔ai-gateway ≥ v1.4.0(contract-tested againstv1.4.5) - Python support: 3.9 – 3.13
- Only runtime dependency:
httpx - License: Apache-2.0
ferrolabs-python-sdk/
├── ferrolabsai/ # Main package (publishes `ferrolabsai` to PyPI)
│ ├── __init__.py # Public API surface — all exports live here
│ ├── _version.py # __version__ constant
│ ├── client.py # FerroClient + AsyncFerroClient, retry policy, error mapping
│ ├── streaming.py # Stream / AsyncStream SSE wrappers
│ ├── types.py # Dataclass response models (ChatCompletion, Usage, ModelInfo, ...)
│ ├── types_responses.py # Response dataclass (Responses API), re-exported from types
│ ├── completions/ # chat.completions (resource.py + async_resource.py)
│ ├── embeddings/ # embeddings
│ ├── images/ # images.generate
│ ├── models/ # model catalog — client-side list/retrieve/search
│ ├── responses/ # /v1/responses create/retrieve/delete
│ ├── moderations/ # /v1/moderations
│ ├── admin/ # Admin API (keys, config, logs, providers, plugins, audit)
│ └── exceptions/ # Exception hierarchy
├── integrations/ # Sibling framework adapter packages (own pyproject.toml each)
│ ├── README.md # Layout + publishing overview
│ ├── langchain-ferrolabsai/ # Publishes `langchain-ferrolabsai` to PyPI (0.2.0, on ferrolabsai 0.3)
│ │ ├── pyproject.toml / README.md / CHANGELOG.md / LICENSE
│ │ ├── langchain_ferrolabsai/{__init__,chat_models,embeddings,llms,_messages}.py
│ │ └── tests/
│ └── llama-index-llms-ferrolabsai/ # Publishes `llama-index-llms-ferrolabsai` (placeholder 0.0.1)
│ ├── pyproject.toml / README.md / CHANGELOG.md / LICENSE
│ ├── llama_index/llms/ferrolabsai/__init__.py # PEP 420 namespace package
│ └── tests/test_placeholder.py
├── tests/
│ ├── conftest.py # Shared fixtures + canned gateway payloads
│ ├── test_client.py # Construction, retries, error mapping, header metadata, probes
│ ├── test_chat.py # Chat body, parsing, streaming (sync + async)
│ ├── test_resources.py # Embeddings, images, models, responses
│ ├── test_admin.py # /admin/* parity
│ └── contract/ # Real-gateway suite (skipped unless FERRO_CONTRACT_BASE_URL)
│ ├── conftest.py
│ ├── stub_upstream.py # stdlib fake OpenAI the gateway is pointed at
│ └── test_contract.py
├── scripts/with-gateway.sh # Builds ferrogw from ../ai-gateway, boots it + the stub, runs tests/contract
├── docs/
│ └── architecture.md # SDK architecture, gateway contract, request lifecycle
├── pyproject.toml # Build config, dependencies, tool settings (ferrolabsai)
├── Makefile # Dev shortcuts: install, test, lint, format, build, contract, clean
├── .github/workflows/
│ ├── ci.yml # Unit matrix + contract job + publish
│ ├── publish-langchain-ferrolabsai.yml # Test + publish on `langchain-ferrolabsai-vX.Y.Z` tags
│ └── publish-llama-index-llms-ferrolabsai.yml # Test + publish on `llama-index-llms-ferrolabsai-vX.Y.Z` tags
├── README.md
├── CONTRIBUTING.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── SECURITY.md
└── LICENSE
Framework adapter packages live under integrations/ as independently versioned and independently published Python distributions. Each sub-folder has its own pyproject.toml, README, CHANGELOG, LICENSE, source tree, and pytest suite, and is built with Hatchling exactly like the parent SDK.
| Package | Folder | Tag prefix | Publish workflow |
|---|---|---|---|
langchain-ferrolabsai |
integrations/langchain-ferrolabsai/ |
langchain-ferrolabsai-vX.Y.Z |
.github/workflows/publish-langchain-ferrolabsai.yml |
llama-index-llms-ferrolabsai |
integrations/llama-index-llms-ferrolabsai/ |
llama-index-llms-ferrolabsai-vX.Y.Z |
.github/workflows/publish-llama-index-llms-ferrolabsai.yml |
Conventions:
- Independent versioning. Each sub-package's
pyproject.tomlversion is bumped on its own cadence. Do not piggy-back on the parent SDK'sv*.*.*tag. - Tag prefix pattern. Releases are cut by pushing tags like
langchain-ferrolabsai-v0.2.0. The publish workflow asserts the tag matches the sub-package'spyproject.tomlversion before uploading. - Trusted Publishing. PyPI credentials use OIDC environments named
pypi-langchain-ferrolabsaiandpypi-llama-index-llms-ferrolabsai— provision these on PyPI before the first publish. - Dependency on the core SDK. Each sub-package declares
ferrolabsai>=X.Y.Zas a runtime dependency (langchain-ferrolabsaineeds>=0.3.0). llama_indexnamespace package.llama-index-llms-ferrolabsaiuses the PEP 420 implicit namespace package layout (llama_index/llms/ferrolabsai/) so it can later be mirrored upstream intorun-llama/llama_indexwith no code changes.- Placeholder behaviour. While a sub-package is at
0.0.x, its__init__.pyexposes only__version__; any attempt to import the planned public classes raisesNotImplementedErrorwith a roadmap link. Real classes land at0.1.0. - Release flow.
make buildandmake testfrom the sub-folder; bump version in itspyproject.toml+CHANGELOG.md; commit + push tag with the prefix above; CI runs the full test matrix and publishes via Trusted Publishing.
python -m venv .venv
source .venv/bin/activate
make install # pip install -e ".[dev]"Dev dependencies: pytest, pytest-asyncio, pytest-httpx, mypy, ruff.
| Command | Purpose |
|---|---|
make install |
Editable install with dev extras |
make test |
Run the unit suite (contract dir auto-skips) |
make lint |
Run ruff check + mypy |
make format |
Run ruff format |
make build |
Build sdist + wheel into dist/ |
make contract |
Boot a real gateway from ../ai-gateway and run tests/contract |
make clean |
Remove build artifacts and tool caches |
Always run make format lint test before committing; run make contract when touching anything that talks to the gateway.
- Python 3.9+ syntax — use
dict[str, X],X | None,from __future__ import annotations. - Type annotations are mandatory on every public function, method, and class attribute.
mypy --strictmust pass on every CI leg (3.9 – 3.13). - Ruff handles linting and formatting. Line length is 100 characters. Select rules:
E,F,I,UP. - Do not hand-format — run
make format.
- Dataclass response models — all types in
types.pyare@dataclasswith afrom_dict()classmethod. No pydantic dependency. - Resource pattern — each API area lives in its own sub-package with a
resource.py(sync) andasync_resource.py. The async module imports the body builders / path constants from the sync one so the wire format is written once. - Client holds HTTP —
_request()(retry + error mapping + header metadata on inference paths) and_open_stream()(SSE, never retried) are the only HTTP entry points. Resources receive the typed client (if TYPE_CHECKING: from ..client import FerroClient) and callself._client._request(...). - Exception hierarchy — all HTTP errors raise typed exceptions inheriting from
FerroAPIError.429, connection errors, and connect timeouts retry for every method;408/5xxand read/write/pool timeouts retry only for idempotent methods (_should_retry). Jittered backoff honoursRetry-After; exhaustion raisesFerroConnectionError/ the mappedFerroAPIError. - Immutability by default — do not mutate arguments; return new objects (
_with_response_metadatareturns a new dict, streaming usesdataclasses.replace). - Keep files small — prefer several focused modules over one large file (
types_responses.pyexists for that reason).
- All public exports must be listed in
ferrolabsai/__init__.pyand the__all__list. - The SDK mirrors the OpenAI SDK surface:
client.chat.completions.create(),client.embeddings.create(),client.images.generate(),client.models.list(),client.responses.create(). - Gateway-specific surface:
client.health()/ready()/live()/capabilities()/rerank(),client.moderations,client.admin.*. - Only model what the gateway really does. Response metadata comes from
X-Request-ID,X-Gateway-Provider,X-Gateway-Overhead-Ms, and the body fieldsprovider/provider_metadata/reasoning_content/ usage counters. There is no cost, cache-hit, or latency field for callers, and no per-request routing-tag or prompt-template request field — do not reintroduce them.docs/architecture.md§ "Gateway Contract" is the reference; the contract suite enforces it.
FERRO_API_KEY— primary API key (takes precedence).OPENAI_API_KEY— fallback for migration.FERRO_BASE_URL— gateway address (defaults tohttp://localhost:8080).
- Unit tests live in
tests/test_*.py; shared fixtures and canned gateway payloads intests/conftest.py. - All HTTP is mocked using
pytest-httpx— no real gateway or network access is needed. tests/contract/runs against a real gateway and is skipped unlessFERRO_CONTRACT_BASE_URLis set;scripts/with-gateway.sh(ormake contract) sets everything up. Every README observability claim is asserted there.- Async tests use
pytest-asynciowithasyncio_mode = "auto". - Every bug fix needs a regression test. Every new feature needs unit tests, and a contract assertion if it touches a gateway route.
- Target 80%+ coverage on new code.
- Run:
make testorpytest tests/ -v --tb=short.
- CI workflow:
.github/workflows/ci.yml - Unit tests, ruff, and mypy run on Python 3.9, 3.10, 3.11, 3.12, 3.13 on
ubuntu-latest. - Contract job checks out
ferro-labs/ai-gatewayatv1.4.5(required) andmain(continue-on-error) and runsscripts/with-gateway.sh. Raise the pin when the SDK starts depending on newer gateway behaviour and update the README compatibility line. - Publishing: Triggered by semver tags (
v*.*.*);needsthe unit matrix and the contract job. Uses PyPI trusted publishing (OIDC). Asserts the tag matchespyproject.tomlversion. - PRs target
developmentbranch; releases are cut frommain.
- Feature branches are created from
development. - PRs target
development; squash-merged by default. - Releases are cut from
main. - Follow Conventional Commits:
feat:,fix:,refactor:,docs:,test:,chore:,perf:,ci:. - Keep subjects under 72 characters.
- Create a new sub-package under
ferrolabsai/(e.g.,ferrolabsai/newresource/). - Add
resource.pywith a class that takesclient: FerroClientin__init__(imported underTYPE_CHECKING), aPATHconstant, and a module-level body builder. - Use
self._client._request(method, path, ...)for HTTP calls. If the route returns inference metadata, add its prefix to_INFERENCE_PREFIXESinclient.py. - Return typed dataclass models — add them to
types.pywithfrom_dict(). - Wire the resource into
FerroClient.__init__inclient.py. - Export new types from
ferrolabsai/__init__.pyand add to__all__. - Add the async variant in
async_resource.py(reusing the sync builder), wire intoAsyncFerroClient. - Write unit tests in
tests/test_<area>.pywithpytest-httpxmocks, and a contract test intests/contract/test_contract.py(extendstub_upstream.pyif the route needs an upstream). - Update
CHANGELOG.mdunderUnreleasedand the route tables inREADME.md/docs/architecture.md.
- Do not add runtime dependencies beyond
httpxwithout discussion. The SDK is intentionally lightweight. - Do not import pydantic — response models are plain dataclasses.
- Do not hardcode secrets in code, tests, or fixtures.
- Do not call
GET /v1/models/{id}— it is not a native gateway route; it falls through to the/v1/*pass-through with the operator's provider credential.models.retrieve()is a client-side lookup for that reason. - Header metadata is for inference bodies only.
_with_response_metadatamust never touch/v1/models, probe, or/admin/*bodies. - Streaming is never retried and must keep the
httpx.Responsereachable (Stream.response). from __future__ import annotationsmust be at the top of every module for 3.9 compatibility withX | Nonesyntax.
In-depth design docs live in docs/:
| Document | Covers |
|---|---|
docs/architecture.md |
Module map, resource pattern, retry policy, streaming, the gateway contract (headers, body fields, catalog), admin route table, error handling |
Read architecture.md before making structural changes to the SDK.
- ferro-labs/ai-gateway — The backend gateway (Go). The SDK talks to its HTTP API.
- Admin API routes are defined in the
internal/admin/handlerspackage (server.go,Handlers.Routes) in the gateway repo; the public routes ininternal/httpserver/router.go.