/v1 surface for chat and embeddings and the OpenAI-compatible surface at
/openai/v1 for images. Roadie holds your provider keys (BYOK), meters usage
per device, and enforces free-vs-pro limits. RoadieKit requires iOS 18+.
Install
Add RoadieKit with Swift Package Manager. In Xcode, choose File → Add Package Dependencies… and enterhttps://github.com/paroaria/roadiekit.git and pick the Up to Next Major rule from
0.2.0, or add it to Package.swift:
Configure the client
ARoadie client needs a RoadieConfig (where the gateway lives, and which image model to request)
and a token provider (how each request is authenticated). Swap the provider between local
development and production without changing any call site:
StaticTokenProvider— a fixed bearer read from the run scheme’sROADIE_STATIC_TOKENenvironment variable (anrd_sk_…key or a pre-minted client token). The working simulator / dev path.AppAttestTokenProvider— silently mints a per-device client token via Apple App Attest, using the publishable key. The production path; it needs a real device (App Attest is unavailable in the simulator).
RoadieConfig
Method surface
TheRoadie client is an actor exposing three surfaces — chat, embeddings, and images — each a
nonisolated, Sendable value type you call directly (no actor hop).
Every surface shares one authorization path (the
TokenProvider below), one idempotency key per logical
call, a single re-mint on a 401, and a transient-failure retry.
Chat
Send a message
ChatRequest takes a model (an alias like "smart" or an explicit "provider/model" id), the
messages array, and optional temperature, maxOutputTokens, and user (an EndUser(id:plan:) for
per-end-user attribution and limits). Build messages with the .system / .user / .assistant /
.tool(_:toolCallId:) helpers. The SDK sets stream for you.
ChatResponse surfaces text (the concatenated reply — a convenience for message.text), the resolved
model / provider, finishReason, token usage, estimated cost, the gateway requestId, and any
message.toolCalls the model requested.
Stream a reply
chat.stream(_:) returns an AsyncThrowingStream of typed events; concatenate the contentDeltas to
render the reply as it arrives:
messageEnd (or a terminal
error event). A mid-stream failure is surfaced as a .error(RoadieError) — the last event, nothing
follows it — while transport and pre-stream HTTP failures are thrown. Cancelling the surrounding task
tears down the underlying connection.
Embeddings
input accepts a single String or a [String]. vectors returns the embeddings in input order
(providers may return them out of order; RoadieKit restores it by index). dimensions is optional for
models that support truncation, and user attributes usage to an end user.
Images
Both image methods return the generated image’s raw bytes (PNG unless the server’soutput_format
is changed) — the caller turns them into a UIImage.
size is an ImageSize (.auto, .square, .landscape, .portrait), background is an
ImageBackground (.transparent, .opaque, .auto), and quality is an ImageQuality (.low,
.medium, .high, .auto). n defaults to 1. On edit, mask is optional; when present, only
the masked region is regenerated. edit defaults background to .transparent; generate defaults
it to .opaque.
Edit an image
Generate from a prompt
Authentication providers
RoadieKit resolves theAuthorization: Bearer … value through a TokenProvider, shared by every
surface:
StaticTokenProvider(token:)returns a fixed token;invalidateToken()is a no-op, so the built-in single remint-retry cannot loop.AppAttestTokenProvider(config:)mints short-lived client tokens by attesting the device with Apple App Attest and exchanging the proof at the gateway’s/v1/device/*endpoints (authorized with the publishable key). Only the App Attest key id is persisted (Keychain); the token lives in memory and is reminted before expiry (or after a 401). It requires apublishableKeyin the config and a real device.
On a DeviceCheck-capable device,
AppAttestTokenProvider also attaches an optional per-device
DeviceCheck token to the mint so the gateway’s free-tier guard can resist reinstall farming. This is
wired internally — there is no separate public DeviceCheck provider to construct — and it fails
open (an unsupported device or a token-generation error simply omits it). See
Mobile device auth.Error handling
Every HTTP or decoding failure is thrown as aRoadieError; task cancellation is Swift’s own
CancellationError (never a RoadieError). The same RoadieError.kind model covers chat, embeddings,
and images:
RoadieError also exposes statusCode, apiType, apiCode, message, requestId, and
isRetryable, and conforms to LocalizedError (errorDescription).
RoadieKit handles two cases for you: it re-mints once on a 401 (an expired client token) and
retries transient network and 5xx failures. A client .timedOut is deliberately not retried —
generation can run for tens of seconds, so a timeout usually means the server is still working, and a
retry would double-execute an expensive call. An .insufficientQuota error drops the cached token and
is re-thrown unchanged (no retry) — your cue to show the paywall; see
Free → Pro with StoreKit.
Not included
- Request-side tool definitions — the chat surface parses tool-call responses (
message.toolCallsand the streamedtoolCallDelta), but declaringtools/tool_choice/response_formaton the request is not yet modeled. For tool-calling and structured-output flows, drive them from a server-side SDK (@roadie/sdkorroadiefor Python). - A separate DeviceCheck provider — DeviceCheck is an internal detail of
AppAttestTokenProvider, not a public API you construct.
Next steps
iOS quickstart
Add RoadieKit to an app end to end.
Send a message to OpenAI
Set up the SDK, send a chat message through Roadie, and get a reply.
Mobile device auth
How App Attest mints per-device client tokens with no account.
Free → Pro with StoreKit
Convert the free tier to a paid subscription and flip the device’s plan.
Android SDK — Roadie
The Kotlin SDK for Android, with chat, embeddings, images, and Play Integrity device auth.