Infuse any API into Elixir: official-quality Elixir SDKs, generated from an OpenAPI spec.

Status: early. Nothing is on Hex yet. Infusion already generates complete SDKs for OpenAI, Stripe, GitHub, Twilio and Cloudflare that compile without a single warning, and the generated GitHub SDK makes live API calls. Streaming and pagination work; release prep is next.

$ mix infusion.gen https://raw.githubusercontent.com/openai/openai-openapi/main/openapi.yaml --name OpenAI
Generated OpenAI in openai_sdk/: 388 operations, 2272 schemas, 2754 files (5828 ms)
{:ok, completion} =
  OpenAI.Chat.create_completion(
    model: "gpt-5.5",
    messages: [%{role: "user", content: "Say hello"}]
  )

[%{message: %{content: text}}] = completion.choices

# Streaming, as a plain Elixir Stream of typed events
OpenAI.Chat.create_completion_stream!(model: "gpt-5.5", messages: messages)
|> Stream.flat_map(& &1.choices)
|> Enum.each(&IO.write(&1.delta.content || ""))

# Every item of a list, across pages, fetched as you read
GitHub.Repos.list_for_org_all!("elixir-lang", per_page: 100)
|> Enum.map(& &1.name)

# Typed structs in, typed errors out
case GitHub.Repos.get("elixir-lang", "elixir") do
  {:ok, %GitHub.Schemas.FullRepository{stargazers_count: stars}} -> stars
  {:error, %Infusion.Runtime.Error{type: :not_found}} -> nil
end

Why

Most API companies no longer write their SDKs by hand. They generate them with Stainless, Fern or Speakeasy, and none of those support Elixir. So when a new API ships, Python, TypeScript, Go and Java developers get an official SDK on day one, and Elixir developers get nothing, or a community client that falls behind.

The open-source generators that do target Elixir don't produce SDKs you can use for a modern API. Measured on OpenAI's published spec (OpenAPI 3.1, 388 operations, 2,272 schemas):

oapi_generator 0.4.0OpenAPI Generator 7.17 (elixir)Infusion
Reads the speccrashesrejects it unless forcedyes, in about 6 s
HTTP clientnot generated: you write ityes (Tesla)yes (Req), with retries
Union types (string or list of parts)reduced to [map]an empty structreal unions, decoded
Compiler warningsn/an/a0
Streaming (SSE)nonoyes, as an Elixir Stream

The goal: SDKs good enough that an API company could ship them as its official Elixir SDK.

Measured on five real specs

Every SDK below is generated from the vendor's own published spec and compiles with zero warnings, checked by the test suite.

APIOperationsModulesGenerated inCompiled in
OpenAI3882,7504 s3 min
Stripe6121,7713 s1.6 min
GitHub1,2813,0549 s2.6 min
Twilio1972390.5 s7 s
Cloudflare3,77810,25313 s3.7 min

Between them these specs cover OpenAPI 3.0 and 3.1, a 28 MB spec, thousands of allOf/anyOf/oneOf, form-encoded bodies, multipart uploads, per-operation servers and server variables. See the spec survey.

Try it

Infusion isn't on Hex yet, so run it from a checkout:

$ git clone https://github.com/mandar1045/infusion && cd infusion
$ mix deps.get
$ mix infusion.gen path/or/url/to/openapi.json --name GitHub --runtime-path runtime
$ cd github_sdk && mix deps.get && mix compile
OptionDefault
--namerequiredthe base module, e.g. OpenAI
--out<name>_sdkwhere the project is written
--app<name>_sdkthe OTP app name
--version0.1.0the SDK's version
--licenseApache-2.0the SDK's license, for Hex
--env-var<NAME>_API_KEYwhere the API key is read from
--runtime-pathdepend on a local infusion_runtime (needed until it's on Hex)

Anything in the spec Infusion can't represent is listed in infusion_warnings.txt instead of turning into broken code.

Using a generated SDK

Path parameters are positional. Everything else the API takes is one map or keyword list, and options for the call come last:

OpenAI.Models.retrieve("gpt-5.5")
OpenAI.Files.list(purpose: "batch", limit: 10)
OpenAI.Chat.create_completion(%{model: "gpt-5.5", messages: msgs}, timeout: 30_000)

OpenAI.Models.retrieve!("gpt-5.5")                     # raises Infusion.Runtime.Error
OpenAI.Models.retrieve("gpt-5.5", raw: true)           # {:ok, %Req.Response{}}, for headers
OpenAI.request(:get, "/brand/new/endpoint", %{x: 1})   # anything the spec doesn't describe yet

Responses decode into structs. Fields the API added after the SDK was generated land in __extra__ instead of disappearing, and parameters the SDK doesn't know about are still sent.

Configuration comes from the spec, then your config, then the call:

# config/runtime.exs
config :openai_sdk, api_key: System.fetch_env!("OPENAI_API_KEY"), max_retries: 5

# GitHub's spec declares no auth, so say how to send the token
config :github_sdk, auth: :bearer, api_key: {:env, "GITHUB_TOKEN"}

Errors are one struct for every SDK, so you handle them the same way everywhere:

{:error, %Infusion.Runtime.Error{type: :rate_limit, status: 429, request_id: "req_..."}}

type is one of :bad_request, :authentication, :permission_denied, :not_found, :conflict, :unprocessable_entity, :rate_limit, :internal_server, :api_error, :timeout or :connection.

Retries work like the official SDKs: 2 retries on connection errors, timeouts, 408, 409, 429 and 5xx, honouring retry-after-ms, retry-after and x-should-retry, with capped exponential backoff and jitter. If the API names an idempotency header, the key stays the same across retries.

Testing: point the SDK at a Req.Test stub.

# config/test.exs
config :openai_sdk, req_options: [plug: {Req.Test, OpenAI}]

# in a test
Req.Test.stub(OpenAI, &Req.Test.json(&1, %{"id" => "gpt-5.5", "object" => "model"}))
{:ok, %OpenAI.Schemas.Model{id: "gpt-5.5"}} = OpenAI.Models.retrieve("gpt-5.5")

Streaming: an operation that can answer with Server-Sent Events gets a _stream variant that asks the API for a stream (it sends stream: true, or whatever that API uses) and returns a lazy Stream of decoded events:

{:ok, events} = OpenAI.Responses.create_stream(model: "gpt-5.5", input: "Write a haiku")

for %OpenAI.Schemas.ResponseTextDeltaEvent{delta: delta} <- events, do: IO.write(delta)

Failures before the first event (a 429, a bad request) come back as {:error, %Infusion.Runtime.Error{}} and are retried like any call; a stream that has started is never retried. Stopping early (Enum.take/2) closes the connection.

Pagination: a list operation gets an _all variant, a lazy stream of every item that fetches the next page only when you read that far:

{:ok, files} = OpenAI.Files.list_all(purpose: "batch")
files |> Stream.filter(&(&1.bytes > 1_000_000)) |> Enum.take(10)

Specs don't say how an API pages, so Infusion recognises the common styles: cursors (after with has_more, OpenAI and Stripe), returned cursors and tokens (Cloudflare, Stripe search), next-page URLs (Twilio), page numbers and GitHub's Link header, and offsets. Across the five specs that's 888 list operations.

Telemetry: [:infusion, :request, :start | :stop | :exception].

How it's built

OpenAPI 3.0 / 3.1 spec (JSON or YAML, any size)
        │
        ▼
 load ─► resolve $refs ─► normalize ─► intermediate representation
                                      (operations, schemas, unions, auth)
        │
        ▼
 Elixir code generation ─► a complete, formatted Mix project
        │
        ▼
 every generated SDK depends on `infusion_runtime`

infusion_runtime is the small package every generated SDK depends on: HTTP (Req), auth, retries, encoding, decoding, streaming, pagination and errors. Keeping it out of the generated code means a fix there reaches every SDK, and generated code stays short and readable: each operation is a few lines of data plus a function that hands it to the runtime. More in the design.

Roadmap

  • [x] OpenAPI 3.0 and 3.1, JSON and YAML, specs of any size
  • [x] Structs for every schema, real union types, forward compatible
  • [x] One function per operation, with docs and typespecs
  • [x] Runtime: auth, JSON / form / multipart bodies, retries, typed errors, telemetry
  • [x] mix infusion.gen writes a complete Mix project
  • [x] Streaming (text/event-stream) as an Elixir Stream
  • [x] Pagination as lazy streams (cursor, page, offset, Link header)
  • [ ] infusion.exs: rename modules and functions, declare pagination,
    patch the spec where it's wrong
  • [ ] Hand-written code that survives regeneration
  • [ ] A GitHub Action that regenerates an SDK when the vendor's spec
    changes and opens a pull request
  • [ ] Publish infusion, infusion_runtime and the first SDKs on Hex

First SDKs

Popular APIs whose Elixir clients are missing or stale (Hex, November 2026), all of which publish official OpenAPI specs:

  • GitHub: no maintained SDK (the best option is 5.7 years old)
  • Cloudflare: no SDK
  • Twilio: the main client hasn't been released in 2.6 years, yet gets 122k downloads per 90 days

OpenAI is the stress test: its spec uses streaming, uploads and unions throughout. Anthropic would be a natural target, but it no longer publishes its spec.

Development

$ mix test                                  # generator
$ cd runtime && mix test                    # runtime
$ INFUSION_SPECS=path/to/specs mix test     # also generates and compiles the five real SDKs (~12 min)

Prior art

License

Apache 2.0