/v1 for chat and embeddings, and the OpenAI-compatible surface
at /openai/v1 for images. It authenticates each device anonymously — no login
screen and no provider key in the app — via Google Play Integrity. Roadie holds your provider keys
(BYOK), meters usage per device, and enforces free-vs-pro limits.
It ships as two artifacts so you only pull in Play Services when you need them:
roadie-core— the full data-plane surface (chat streaming + non-streaming, embeddings, images, feedback) with static-token auth. A pure Kotlin/JVM library (OkHttp + kotlinx.serialization + coroutines) with no Android-framework dependency, so it builds and unit-tests with just Gradle and a JDK, and is consumable from any Android app.roadie-playintegrity— thePlayIntegrityTokenProviderand its Android Keystore + Play Integrity plumbing. Add it for production device auth; it depends onroadie-core.
The two token providers are interchangeable at the call site — build with
StaticTokenProvider
during development and switch to PlayIntegrityTokenProvider in production without touching a single
chat, embeddings, or images call.Install
Add the artifacts to your app module’sbuild.gradle.kts. roadie-playintegrity brings
roadie-core in transitively, so a production app that uses device auth only needs the one dependency;
a dev-only or server-side integration that supplies a static token can depend on roadie-core alone.
build.gradle.kts
0.x); changes inside /v1 are additive.
Requirements
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 (anrd_sk_…key or a pre-minted client token). The simplest dev path; read it from a build config field or an environment variable, never a committed literal.PlayIntegrityTokenProvider— silently mints a per-device client token via Google Play Integrity, using the publishable key. The production path; it needs a real device with Google Play Services (Play Integrity is unavailable on bare emulators).
RoadieConfig
Method surface
TheRoadie client exposes four resource surfaces — chat, embeddings, images, and feedback.
Every method except chat.stream is a suspend function; chat.stream returns a cold Flow.
Chat
chat.create runs a non-streaming completion and returns the normalized ChatResponse — the assistant
message, the finishReason, token usage, an estimated cost, the resolved provider/model, and
the gateway requestId. ChatResponse.text concatenates the reply’s text parts for you.
Build messages with the
ChatMessage companion helpers — ChatMessage.system(…),
.user(…), .assistant(…), and .tool(content, toolCallId). The wire field for the output cap is
max_output_tokens (maxOutputTokens in Kotlin), not max_tokens. stream is managed by the
SDK — create sends false, stream sends true — so you never set it.Stream a completion
chat.stream returns a cold Flow<ChatStreamEvent>; the HTTP connection is opened lazily when you
start collecting. Concatenate ContentDelta.delta chunks to build the reply, and read the final
usage/cost from the terminal MessageEnd. Cancelling the collector cancels the underlying
connection.
ChatStreamEvent is a sealed interface with one case per SSE frame: MessageStart, ContentDelta,
ToolCallDelta, UsageEvent, MessageEnd, and a terminal Error. Transport, decoding, and
pre-stream HTTP failures are thrown from the flow; a mid-stream error frame is instead yielded
as a terminal ChatStreamEvent.Error(error: RoadieException) — nothing follows it. See
Streaming.
Embeddings
embeddings.create embeds one or more inputs and returns them in input order. EmbeddingResponse.vectors
gives the raw List<List<Double>>; the request accepts either a single String or a List<String>.
Images
The image surface speaks the OpenAI-compatible shape the gateway exposes under/openai/v1. Both
generate and edit return the generated image’s raw bytes (PNG unless the server’s
output_format is changed) — turn them into a Bitmap with BitmapFactory.decodeByteArray.
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. The two operations default background differently —
generate defaults to OPAQUE, edit to TRANSPARENT.
Feedback
feedback.submit records a thumbs rating for a prior chat or embeddings response, keyed by that
response’s gateway requestId.
Authentication providers
Every request resolves itsAuthorization: Bearer … value through a TokenProvider. Swapping the
provider — a static token in dev, PlayIntegrityTokenProvider in production — changes nothing at the
call site.
StaticTokenProvider(token)returns a fixed token;invalidateToken()is the interface default (a no-op), so the transport’s single remint-on-401 retry re-sends the same token exactly once and never loops.PlayIntegrityTokenProvider(context, config, options)mints short-lived client tokens by proving the app is a genuine build on a real device — a Google Play Integrity classic verdict plus a hardware-backed Android Keystore EC key — and exchanging that proof at the gateway’s/v1/device/android/{challenge,attest,assert}routes (authorized with the publishable key). No end-user account and no secret key; the device becomes the end user. It requires apublishableKeyin the config and a real device with Google Play Services. Only the durable device key (Android Keystore) and the onboarding time (EncryptedSharedPreferences) persist; the client token lives only in memory and is re-minted before expiry (or after a 401).
Attest, assert, and re-attest
The first run — and any run whose last attest is older thanPlayIntegrityOptions.attestRefreshInterval (default 12 hours) — performs a full attest: a
fresh Play Integrity verdict. Every other mint uses the cheap Keystore assert, keeping Play
Integrity off the hot path. The periodic re-attest is what lets the gateway (re)write the durable
Device Recall “free consumed” bit within the refresh window after a device exhausts its quota —
Roadie’s reinstall-proof free-tier guard (the Android analog of iOS DeviceCheck).
refreshAttestation() forces an immediate attest. Call it when the device has just become relevant to
the free-tier guard — for example, right before showing a paywall — so the gateway writes the Device
Recall bit now rather than at the next refresh window.
PlayIntegrityOptions carries the tuning beyond the gateway location + publishable key:
Device Recall is wired internally — there is no separate provider to construct — and it fails
open (an unsupported device or a write failure simply grants the normal free tier and never blocks a
mint). Enabling it is an operator step: see
Android device auth setup and
Mobile device auth.
Error handling
Every HTTP or decoding failure is thrown as aRoadieException; a cancelled coroutine surfaces
kotlinx.coroutines.CancellationException, which the SDK never wraps. RoadieException is a sealed
class, so branch with is / when:
type): InvalidRequestException,
AuthenticationException, PermissionException, NotFoundException, RateLimitException,
QuotaException, BudgetException, ProviderException, IdempotencyException, InternalException.
A content-policy rejection maps to InvalidRequestException. Client-side errors:
ApiConnectionException, ApiConnectionTimeoutException (a subclass of ApiConnectionException, but
deliberately not retried), ApiDecodingException, and RoadieConfigurationException.
Every RoadieException carries status, code, requestId, param, docUrl, and — for a rate
limit — retryAfter.
The SDK handles two cases for you: it re-mints once on a 401 (an expired client token) and
retries transient network and 5xx failures, honoring Retry-After and reusing one auto
Idempotency-Key across those retries. A client timeout (ApiConnectionTimeoutException) is
deliberately not retried — an image generation runs for tens of seconds, so a timeout usually means
the server is still working, and a retry would double-execute an expensive call. A QuotaException
(insufficient_quota) drops the cached token and is re-thrown unchanged (no retry) — your cue to show
the paywall. See Errors & retries and the full
error reference.
Next steps
Android quickstart
Add the SDK to an app end to end and send your first chat.
Android device auth setup
Configure Google Play Integrity so the SDK can mint per-device tokens.
Mobile device auth
How anonymous device attestation mints client tokens with no account.
Free → Pro entitlements
Turn a free device into a paying user.