Changelog
August 29, 2026 · View on GitHub
All notable changes to ferrolabsai will be documented here.
Format follows Keep a Changelog. This project adheres to Semantic Versioning.
[0.3.0] — 2026-08-29
The "truth release": every claim the SDK makes now matches what
ai-gateway actually does, and a
contract suite (tests/contract/, run in CI against v1.4.5 and main)
keeps it that way. Compatibility: ferrolabsai 0.3.x ↔ ai-gateway ≥ v1.4.0.
Breaking
- Response metadata reads the gateway's real headers.
trace_idcomes fromX-Request-ID,providerfrom the chat body'sproviderfield (orX-Gateway-Provideron responses/pass-through), and the newgateway_overhead_msfromX-Gateway-Overhead-Ms. The legacyx-trace-id/x-ferro-request-idheader fallbacks and thex_ferro_*body keys are gone. - Removed
ChatCompletion.latency_ms,Usage.cost_usd,Usage.cache_hit,Usage.provider. They were read fromX-Ferro-Latency-Ms/X-Ferro-Cost-Usd/X-Ferro-Providerheaders that the gateway has never emitted at any version, so they were alwaysNone.gateway_overhead_msis not a rename oflatency_ms: it is the gateway's own processing time, not end-to-end latency. Cost and cache hits remain available in the request log (admin.logs.list()), Prometheus, and OTel — not to callers. - Removed
route_tag/x_route_tag,template_id,template_variablesfromchat.completions.create(). The gateway never decoded them (internal/handler/chatrequest.go); unknown kwargs still pass through verbatim. ModelInfois the gateway'sEnrichedModelInfo.provideris now a read-only alias of the newowned_by;input_cost_per_token/output_cost_per_tokenare removed (never served); addedcreated,mode,max_output_tokens,deprecated;capabilitiesdefaults to[].models.retrieve(id)no longer callsGET /v1/models/{id}. That path is not a native gateway route — it fell through to the/v1/*pass-through and was forwarded upstream with the operator's provider credential. It is now a client-side lookup overGET /v1/modelsand raisesFerroNotFoundError(code="model_not_found") locally.models.list(provider=, capability=)andmodels.search()filter client-side too (the gateway ignores query parameters);providermatchesowned_by,capabilitymatchescapabilities[],searchis a case-insensitive substring onid.- Streaming returns a
Stream/AsyncStreamobject instead of a bare generator. It iterates exactly as before and additionally exposestrace_id,provider,response,close(), and context-manager support. HTTP errors on a stream now raise whencreate(stream=True)is called (async: when awaited), not on first iteration. - Retries are idempotent-aware. HTTP
429, connection errors, and connect timeouts are retried for every method; HTTP408/5xxand read / write / pool timeouts are retried only forGET/HEAD/PUT/DELETE/OPTIONS. APOSTthat hits a read timeout is no longer retried (0.2.x retried every timeout regardless of method) — it may already have been processed. Full-jitter backoff,Retry-Afterhonoured (capped at 30 s; negative,NaN, and infinite values are ignored).max_retries=0disables retries. Streaming is never retried, and its transport failures now raiseFerroConnectionErrorlike_requestdoes. FerroRateLimitError.__init__gained keyword-onlyretry_after;FerroStreamError.__init__gained keyword-onlycode._request(stream=True)(private) is removed;_open_stream()replaces it.
Added
stream_optionsparameter (e.g.{"include_usage": True});ChatCompletionChunk.usage,.trace_id,.provider;Usage.reasoning_tokens,.cache_read_tokens,.cache_write_tokens;ChatMessage.reasoning_content,StreamDelta.reasoning_content;ChatCompletion.provider_metadata. Mid-stream{"error": ...}frames raiseFerroStreamErrorwith.code(stream_error,stream_timeout).- Request params
max_completion_tokens,parallel_tool_calls,response_format,seedonchat.completions.create()(sync + async). FerroBudgetExceededError(402insufficient_quota) andFerroPermissionError(403insufficient_scope); everyFerroAPIErrornow carriesstatus_codeand the gateway'scode.client.responses.create() / retrieve(id) / delete(id)→/v1/responses(Responsedataclass; id routes answer 501responses_not_configuredunless the gateway setsresponses_target).client.capabilities()→/v1/capabilities;client.health(),client.ready(),client.live()→/health,/readyz,/livez(503 bodies are returned, not raised);client.rerank()→/v1/rerank;client.moderations.create()→/v1/moderations.- Admin parity with ai-gateway 1.4:
admin.audit.list(),admin.providers.catalog(),admin.plugins.catalog(),admin.logs.list(api_key_id=)(andstage="all"),admin.logs.stats(buckets=). EmbeddingResponse.trace_id,ImageResponse.trace_id.- Python 3.13 in the CI matrix and classifiers; mypy runs on every leg.
- Contract suite:
scripts/with-gateway.sh+tests/contract/(stub upstream, 23 assertions against a real gateway);make contract. - Unit tests for the previously open good-first-issues (#8 keys
retrieve/update, #9 config create/delete, #10 logs delete, #12 sync
models.search/images.generate/admin.health/providers.list, #14 retry
exhaustion →
FerroConnectionError, #19 async streaming + embeddings).
Fixed
- Header metadata is merged into inference bodies only;
/v1/models, probes, and/admin/*bodies are returned untouched. admin.providers.catalog()unwraps the gateway's{"providers": [...]}envelope.__version__is a constant kept in sync withpyproject.toml(the installed-distribution lookup reported a stale number in editable checkouts).- Resource classes type their
client(no moreAny/# type: ignore[no-any-return]). - README: "29 providers" → 30 ;
admin.logs.list(trace_id=...)example (no such parameter) replaced; deadinternal/admin/handlers.golinks → theinternal/admin/handlerspackage; observability section rewritten to list exactly what populates and from where; framework section now points atlangchain-ferrolabsai.
Removed
- Everything listed under Breaking:
latency_ms,cost_usd,cache_hit,Usage.provider,route_tag,template_id,template_variables,ModelInfopricing fields,x_ferro_*/X-Ferro-*/x-trace-idhandling. None of it was ever provided or read by the gateway.
[0.2.1] — 2026-06-13
Fixed
- Streaming completions now raise
FerroStreamErroron malformed SSE chunks instead of silently dropping them, for both the sync and async clients. This is the first code path that actually raises the previously-unusedFerroStreamError.
Added
- Test coverage for
admin.dashboard()andadmin.plugins.list()(bare-array,{"data": [...]}, and{"plugins": [...]}response shapes). - Streaming malformed-chunk regression tests for the sync and async clients.
[0.2.0] — 2026-05-14
Added
- Awaitable async resources for models, images, and admin endpoints:
async_client.models.*,async_client.images.generate(), andasync_client.admin.*now use async request paths instead of sync resource classes. - Capped exponential retry backoff for sync and async connection/timeout retries.
py.typedmarker for downstream type checkers.
Changed
- Package version lookup now uses package metadata through a shared version
helper, keeping
__version__and the client user agent aligned. route_tag,template_id, andtemplate_variablesremain forwarded request fields, but are documented as gateway-dependent until OSS gateway support is confirmed.
Fixed
- Integration package publish workflows now use valid GitHub tag glob patterns, while keeping exact tag/version validation in the publish job.
- Async requests now return
{}for204 No Contentand other empty-body responses, matching sync client behavior. - Async custom HTTP clients merge SDK auth/default headers, matching sync client behavior.
- Request IDs propagate from error response headers or
trace_idresponse bodies into Ferro exceptions.
[0.1.0] — 2026-04-09
Added
FerroClient— synchronous client with OpenAI-compatible interfaceAsyncFerroClient— async client usinghttpx.AsyncClientclient.chat.completions.create()— streaming and non-streamingclient.embeddings.create()— text embeddingsclient.images.generate()— image generationclient.models.list()/.retrieve()/.search()— 2,500+ model catalogclient.admin.keys— API key CRUD (list, retrieve, create, update, delete, revoke, rotate, usage) backed by/admin/keysclient.admin.config— single-active routing config:get,create,update,delete,history,rollbackbacked by/admin/configclient.admin.logs— request log query / stats / prune backed by/admin/logsclient.admin.providers,client.admin.plugins,client.admin.dashboard(),client.admin.health()- Ferro-specific extras:
template_id,template_variables,route_tag,cost_usd,provider - Full exception hierarchy:
FerroAuthError,FerroRateLimitError,FerroNotFoundError,FerroServerError,FerroConnectionError - Auto-retry on connection errors (configurable
max_retries) - Context manager support (
with FerroClient(...) as client:) - Environment variable support:
FERRO_API_KEY,FERRO_BASE_URL,OPENAI_API_KEYfallback - Test suite with 100% mocked HTTP (no real gateway needed)
- GitHub Actions CI across Python 3.9–3.12 with PyPI trusted publishing