Android SDK
The official nRouter Android SDK packaged as an AAR with coroutines support, main-thread safety, and automatic response metadata extraction.
Last updated
The official nRouter Android SDK (ai.nrouter:nrouter-sdk-android) provides idiomatic Kotlin coroutine bindings for Android applications. Built on top of the shared Kotlin core client, it delivers main-thread safety, automatic lifecycle-safe dispatching, and response metadata extraction while connecting to https://api.nrouter.ai/v1.
Source Installation
Android is currently a source preview rather than a Maven Central release. Build the same-version Kotlin core and Android AAR into your local Maven cache first:
cd sdks/kotlin && ./gradlew clean check publishToMavenLocal
cd ../android && ./gradlew clean build publishToMavenLocalAdd the dependency to your application's build.gradle.kts:
// app/build.gradle.kts
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation("ai.nrouter:nrouter-sdk-android:3.1.2")
}The package declares android.permission.INTERNET and bundles consumer ProGuard rules automatically, so no additional R8 configuration is required. Minimum supported Android version is API 21 (minSdk = 21).
Setup & Authentication
Because System.getenv returns null on Android devices, do not rely on environment variables at runtime. Pass your virtual API key directly to the factory method:
import ai.nrouter.sdk.android.NRouterAndroid
// In production, pass a short-lived key minted by your backend:
val client = NRouterAndroid.create(context, "sk-nrouter-your-api-key-here")For development and local unit testing via the command line, export the environment variable:
export NROUTER_API_KEY="sk-nrouter-your-api-key-here"Requests target https://api.nrouter.ai by default.
Key Safety in Mobile Applications
Anything bundled inside an APK or Android app bundle (BuildConfig, strings.xml, or AndroidManifest meta-data) can be extracted by users. Never hardcode long-lived production keys in client binaries. For production deployments, your backend service should mint a short-lived virtual key and deliver it to authenticated clients.
For prototyping or internal builds, manifest configuration is supported:
<application>
<meta-data android:name="ai.nrouter.sdk.API_KEY" android:value="sk-nrouter-..." />
</application>val client = NRouterAndroid.create(context)Quickstart
Make asynchronous chat completions from an AndroidViewModel or coroutine scope:
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import ai.nrouter.sdk.android.NRouterAndroid
import kotlinx.coroutines.launch
import org.json.JSONObject
class ChatViewModel(app: Application) : AndroidViewModel(app) {
private val client = NRouterAndroid.create(app, apiKey = "sk-nrouter-your-api-key-here")
fun sendMessage(prompt: String) = viewModelScope.launch {
// SDK calls automatically switch to Dispatchers.IO
val result = client.chatCompletions(
JSONObject()
.put("model", "gpt-5.4-mini")
.put("messages", listOf(mapOf("role" to "user", "content" to prompt)))
)
val messageContent = result.body
.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("message")
.getString("content")
println("Response: $messageContent")
// Inspect metadata
if (result.meta.isPriced) {
println("Cost: $${result.meta.cost}")
}
}
}Streaming
The Android SDK shares the Kotlin core's cold, cancellable Flow streaming APIs. Each chunk delivers the text delta along with the raw frame and gateway metadata:
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.json.JSONObject
viewModelScope.launch {
client.messagesStream(
JSONObject()
.put("model", "claude-haiku-4-5-20251001")
.put("max_tokens", 256)
.put("messages", listOf(mapOf("role" to "user", "content" to "Hello!")))
).collect { chunk ->
print(chunk.delta)
}
}Cancelling the coroutine job cancels the underlying HTTP call immediately.
Error Handling
All gateway refusals map to typed exceptions under NRouterError:
import ai.nrouter.sdk.NRouterError
try {
val response = client.chatCompletions(requestBody)
} catch (e: NRouterError.GuardrailBlocked) {
// Blocked by configured guardrail policy
println("Request blocked by guardrails: ${e.message}")
} catch (e: NRouterError.Credit) {
// Insufficient credits or budget exhausted
println("Credit check failed: ${e.message}")
} catch (e: NRouterError.RateLimit) {
// Rate limit or TPM limit exceeded
if (e.isRetryable) {
println("Rate limit reached, retryable after backoff")
}
}| Class | HTTP Status | Description |
|---|---|---|
NRouterError.Request | 400 | Invalid request format or parameters |
NRouterError.GuardrailBlocked | 400 | Blocked by server-side guardrail policies |
NRouterError.Authentication | 401 | Invalid or revoked API key |
NRouterError.Credit | 402 | Insufficient balance or budget ceiling hit |
NRouterError.NotFound | 404 | Requested model not found or inactive |
NRouterError.RateLimit | 429 | Rate limit or TPM limit exceeded |
NRouterError.Service | 503 | Upstream service or credit check unavailable |
NRouterError.Transport | — | Network connection failure |
Response Metadata & Cost Tracking
Every response provides parsed x-nr-* gateway headers via result.meta:
requestId(x-nr-request-id): Unique request identifier.latencyMs(x-nr-latency-ms): Milliseconds measured from edge arrival to response headers ready.cost(x-nr-request-cost): Exact USD cost of the request.costStatus(x-nr-cost-status):exactorunpriced.inputTokens/outputTokens: Token usage counts.
Source & Repository
The source code and runnable demonstrations are available on GitHub: