Connection settings for the TypeSafe API, resolved once and passed around.
The client is a plain struct rather than a process: nothing about talking to the API needs shared state, so a struct keeps concurrency trivial (build once, use from any number of tasks) and keeps supervision trees out of your way.
Configuration precedence
api_key, base_url, model, timeout, connect_timeout and finch are
resolved from, in order:
- the options passed to
new/1 - application config:
config :typesafe_api, api_key: "...", model: "..." - environment variables, for the three settings that have one:
TYPESAFE_API_KEY,TYPESAFE_BASE_URL,TYPESAFE_DEFAULT_MODEL - the built-in defaults (
https://api.typesafe.ai,jev-latest, 10 000 ms, 5 000 ms)
retry and req_options are read from new/1 only. They are structured
values, not strings, and a retry policy in config would be resolved at a
point where nothing can tell you it is wrong.
Values are validated wherever they come from, so a stray
config :typesafe_api, base_url: :production fails with a configuration
error at new/1 rather than a FunctionClauseError on the first request.
Blank strings are ignored the same way in both, after trimming.
The API key has no default; new/1 raises ArgumentError when none is found.
Placement
Because it is a plain struct with no process behind it, a client is safe to
share across processes: build one and pass it into every task, GenServer or
controller that needs it. Build it in a function, not in a module attribute.
A module attribute is evaluated at compile time, which reads the API key
from whatever environment compiled the release rather than the one running
it. A function called at application start, or a small Application.get_env
lookup memoized in :persistent_term, keeps configuration at runtime where
it belongs.
req_options merges rather than replaces. The client's req_options are
applied first, then the ones passed to a single call, so a per-call option
wins over the same key on the client. Anything the client sets and the call
does not mention survives.
Connections
Req picks a Finch connection pool by the connect_options it is given,
so two requests with different connect_options do not share a pool.
Varying that option per call spins up a separate pool each time and throws
away connection reuse. Set connect_options once on the client, in
req_options, and leave it out of per-call options. The client's
connect_timeout is merged underneath, so it survives unless you set
timeout: inside connect_options yourself.
For that reason the connect timeout is a client setting, connect_timeout
(5 000 ms by default), and not the per-call :timeout. The per-call
:timeout bounds waiting for the response (receive_timeout) only, so
calls that differ in timeout still share one pool. See TypeSafeAPI.HTTP for
the full list of options that do and do not affect pool identity.
To size the pool yourself, start a Finch in your own supervision tree and
name it with finch::
children = [{Finch, name: MyApp.Finch, pools: %{default: [size: 50, count: 2]}}]
TypeSafeAPI.new(api_key: key, finch: MyApp.Finch)Req refuses :finch and :connect_options together, so a client with
finch: sends neither connect_options nor connect_timeout: connection
settings for a pool you own belong in its own child spec. new/1 raises
rather than letting the two be set at once.
req_options is an escape hatch for Req, not a second way to configure
this library. :retry, :auth, :base_url and :finch are rejected there
with a message naming the client option to use instead; a retry: that
slipped through would run Req's own retry loop nested inside this
library's, multiplying attempts and escaping the wall-clock budget.
The API key is redacted by this module's Inspect implementation, so it
does not leak through inspect/1, a crash dump or a logged struct. It is
never copied into telemetry metadata or into TypeSafeAPI.Error bodies either;
those carry the request as sent minus the Authorization header.
Summary
Functions
Builds a client, resolving configuration as described in the module docs.
Types
@type t() :: %TypeSafeAPI.Client{ api_key: String.t(), base_url: String.t(), connect_timeout: pos_integer(), finch: atom(), model: String.t(), req_options: keyword(), retry: TypeSafeAPI.Retry.t(), timeout: pos_integer() }
Functions
Builds a client, resolving configuration as described in the module docs.
Options
:api_key(String.t/0) - API key. Falls back to app config, thenTYPESAFE_API_KEY.:base_url(String.t/0) - API base URL. Falls back toTYPESAFE_BASE_URL, then the public API.:model(String.t/0) - Default model. Falls back toTYPESAFE_DEFAULT_MODEL, thenjev-latest.:timeout(pos_integer/0) - Per-operation timeout in milliseconds. Defaults to 10 000.:connect_timeout(pos_integer/0) - Timeout in milliseconds for establishing a connection. Defaults to 5 000. Client-level on purpose: it feedsconnect_options, which selects the Finch pool, so it must not vary per call. Ignored whenfinch:is set, and overridden by atimeout:inside your ownconnect_options.:finch(atom/0) - Name of aFinchpool you start and supervise yourself, for control over pool size. Suppressesconnect_optionsandconnect_timeout, whichReqrefuses to combine with a named pool.:retry- Retry policy; seeTypeSafeAPI.Retry.new/1. The default value is[].:req_options- Escape hatch: options merged into the underlyingReq.Request.:retry,:auth,:base_urland:finchare rejected; each has a client option of its own. The default value is[].