Skip to main content
openai-kotlin is a community-maintained Kotlin client for OpenAI APIs. It provides a clean, Kotlin-idiomatic interface with full coroutine support, making it perfect for Android and server-side Kotlin applications.

Installation

Gradle (Kotlin DSL)

dependencies {
    implementation("com.aallam.openai:openai-client:3.8.2")
    implementation("io.ktor:ktor-client-okhttp:2.3.12") // or another Ktor engine
}

Gradle (Groovy)

dependencies {
    implementation 'com.aallam.openai:openai-client:3.8.2'
    implementation 'io.ktor:ktor-client-okhttp:2.3.12'
}

Configuration

import com.aallam.openai.api.http.Timeout
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
import kotlin.time.Duration.Companion.seconds

val config = OpenAIConfig(
    token = "lmnfl_your_api_key",
    host = OpenAIHost(baseUrl = "https://api.lumenfall.ai/openai/v1"),
    timeout = Timeout(socket = 60.seconds)
)

val openAI = OpenAI(config)
Security consideration for Android apps: API keys embedded in Android apps can be extracted by users through APK decompilation, reverse engineering, or debugging tools. Anyone with access to your API key can make requests at your expense.For production apps, consider:
  • Proxy through your backend: Route API calls through your own server that holds the API key securely
  • Per-user authentication: Issue individual API keys to authenticated users with appropriate rate limits
  • Usage monitoring: Set up alerts for unusual usage patterns in your Lumenfall dashboard
  • ProGuard/R8: While obfuscation helps, it does not fully protect embedded secrets

Using environment variables

val config = OpenAIConfig(
    token = System.getenv("LUMENFALL_API_KEY"),
    host = OpenAIHost(baseUrl = "https://api.lumenfall.ai/openai/v1")
)

val openAI = OpenAI(config)

Generate images

import com.aallam.openai.api.image.ImageCreation
import com.aallam.openai.api.image.ImageSize
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI

suspend fun generateImage(openAI: OpenAI): String {
    val images = openAI.imageURL(
        creation = ImageCreation(
            prompt = "A serene mountain landscape at sunset with dramatic clouds",
            model = ModelId("gemini-3-pro-image"),
            n = 1,
            size = ImageSize.is1024x1024
        )
    )

    return images.first().url
}

Generation options

val images = openAI.imageURL(
    creation = ImageCreation(
        prompt = "A beautiful garden with roses",
        model = ModelId("gpt-image-1.5"),
        n = 1,
        size = ImageSize.is1792x1024,  // landscape
        quality = Quality.HD,
        style = Style.Natural
    )
)

Available sizes

SizeDimensions
ImageSize.is256x256256x256
ImageSize.is512x512512x512
ImageSize.is1024x10241024x1024
ImageSize.is1024x17921024x1792 (portrait)
ImageSize.is1792x10241792x1024 (landscape)

Get base64 response

import com.aallam.openai.api.image.ImageCreation
import com.aallam.openai.api.model.ModelId

val images = openAI.imageJSON(
    creation = ImageCreation(
        prompt = "A cute robot painting",
        model = ModelId("gpt-image-1.5"),
        n = 1
    )
)

val base64Image = images.first().b64JSON

Edit images

import com.aallam.openai.api.image.ImageEdit
import com.aallam.openai.api.image.ImageSize
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.api.file.FileSource
import okio.source
import java.io.File

suspend fun editImage(openAI: OpenAI): String {
    val imageFile = File("original.png")

    val images = openAI.imageURL(
        edit = ImageEdit(
            image = FileSource(
                name = "original.png",
                source = imageFile.source()
            ),
            prompt = "Add a rainbow in the sky",
            model = ModelId("gpt-image-1.5"),
            n = 1,
            size = ImageSize.is1024x1024
        )
    )

    return images.first().url
}

With a mask

val imageFile = File("original.png")
val maskFile = File("mask.png")

val images = openAI.imageURL(
    edit = ImageEdit(
        image = FileSource(
            name = "original.png",
            source = imageFile.source()
        ),
        mask = FileSource(
            name = "mask.png",
            source = maskFile.source()
        ),
        prompt = "A sunlit indoor lounge area with a pool",
        model = ModelId("gpt-image-1.5"),
        n = 1,
        size = ImageSize.is1024x1024
    )
)

List models

suspend fun listModels(openAI: OpenAI) {
    val models = openAI.models()

    models.forEach { model ->
        println(model.id.id)
    }
}

Android example

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ImageGeneratorScreen()
        }
    }
}

@Composable
fun ImageGeneratorScreen(viewModel: ImageViewModel = viewModel()) {
    var prompt by remember { mutableStateOf("") }
    val imageUrl by viewModel.imageUrl.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        OutlinedTextField(
            value = prompt,
            onValueChange = { prompt = it },
            label = { Text("Enter prompt") },
            modifier = Modifier.fillMaxWidth()
        )

        Button(
            onClick = { viewModel.generateImage(prompt) },
            enabled = !isLoading && prompt.isNotBlank()
        ) {
            Text(if (isLoading) "Generating..." else "Generate")
        }

        imageUrl?.let { url ->
            AsyncImage(
                model = url,
                contentDescription = "Generated image",
                modifier = Modifier.fillMaxWidth()
            )
        }
    }
}

ViewModel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aallam.openai.api.image.ImageCreation
import com.aallam.openai.api.image.ImageSize
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class ImageViewModel : ViewModel() {
    private val openAI = OpenAI(
        OpenAIConfig(
            token = BuildConfig.LUMENFALL_API_KEY,
            host = OpenAIHost(baseUrl = "https://api.lumenfall.ai/openai/v1")
        )
    )

    private val _imageUrl = MutableStateFlow<String?>(null)
    val imageUrl = _imageUrl.asStateFlow()

    private val _isLoading = MutableStateFlow(false)
    val isLoading = _isLoading.asStateFlow()

    fun generateImage(prompt: String) {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val images = openAI.imageURL(
                    creation = ImageCreation(
                        prompt = prompt,
                        model = ModelId("gemini-3-pro-image"),
                        n = 1,
                        size = ImageSize.is1024x1024
                    )
                )
                _imageUrl.value = images.first().url
            } catch (e: Exception) {
                // Handle error
            } finally {
                _isLoading.value = false
            }
        }
    }
}

Error handling

import com.aallam.openai.api.exception.OpenAIAPIException
import com.aallam.openai.api.exception.OpenAIHttpException

suspend fun generateImageSafely(openAI: OpenAI, prompt: String): String? {
    return try {
        val images = openAI.imageURL(
            creation = ImageCreation(
                prompt = prompt,
                model = ModelId("gemini-3-pro-image"),
                n = 1
            )
        )
        images.first().url
    } catch (e: OpenAIAPIException) {
        when (e.statusCode) {
            401 -> println("Invalid API key")
            429 -> println("Rate limit exceeded")
            402 -> println("Insufficient balance")
            else -> println("API error: ${e.message}")
        }
        null
    } catch (e: OpenAIHttpException) {
        println("Network error: ${e.message}")
        null
    }
}

Concurrent image generation

import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope

suspend fun generateMultipleImages(
    openAI: OpenAI,
    prompts: List<String>
): List<String> = coroutineScope {
    prompts.map { prompt ->
        async {
            val images = openAI.imageURL(
                creation = ImageCreation(
                    prompt = prompt,
                    model = ModelId("gemini-3-pro-image"),
                    n = 1,
                    size = ImageSize.is1024x1024
                )
            )
            images.first().url
        }
    }.awaitAll()
}

Next steps