TypeSafeAPI.Client (TypeSafe AI v0.1.0-alpha.3)

Copy Markdown View Source

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:

  1. the options passed to new/1
  2. application config: config :typesafe_api, api_key: "...", model: "..."
  3. environment variables, for the three settings that have one: TYPESAFE_API_KEY, TYPESAFE_BASE_URL, TYPESAFE_DEFAULT_MODEL
  4. 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

t()

@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

new(opts \\ [])

@spec new(keyword()) :: t()

Builds a client, resolving configuration as described in the module docs.

Options

  • :api_key (String.t/0) - API key. Falls back to app config, then TYPESAFE_API_KEY.

  • :base_url (String.t/0) - API base URL. Falls back to TYPESAFE_BASE_URL, then the public API.

  • :model (String.t/0) - Default model. Falls back to TYPESAFE_DEFAULT_MODEL, then jev-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 feeds connect_options, which selects the Finch pool, so it must not vary per call. Ignored when finch: is set, and overridden by a timeout: inside your own connect_options.

  • :finch (atom/0) - Name of a Finch pool you start and supervise yourself, for control over pool size. Suppresses connect_options and connect_timeout, which Req refuses to combine with a named pool.

  • :retry - Retry policy; see TypeSafeAPI.Retry.new/1. The default value is [].

  • :req_options - Escape hatch: options merged into the underlying Req.Request. :retry, :auth, :base_url and :finch are rejected; each has a client option of its own. The default value is [].