Skip to main content

Installation

  1. Add InsForge dependencies to your project
build.gradle.kts:
repositories {
    mavenLocal() // For local development
    mavenCentral()
}

dependencies {
    implementation("dev.insforge:insforge-kotlin:0.1.5")
}
  1. Initialize InsForge SDK
import dev.insforge.createInsforgeClient
import dev.insforge.auth.Auth
import dev.insforge.database.Database
import dev.insforge.storage.Storage
import dev.insforge.functions.Functions
import dev.insforge.realtime.Realtime
import dev.insforge.ai.AI

val client = createInsforgeClient(
    baseUrl = "https://your-app.insforge.app",
    anonKey = "your-api-key"
) {
    install(Auth)
    install(Database)
    install(Storage)
    install(Functions)
    install(Realtime) {
        autoReconnect = true
        reconnectDelay = 5000
    }
    install(AI)
}
  1. Enable Logging (Optional)
For debugging, you can configure the SDK log level:
import dev.insforge.InsforgeLogLevel

val client = createInsforgeClient(
    baseUrl = "https://your-app.insforge.app",
    anonKey = "your-api-key"
) {
    // DEBUG: logs request method/URL and response status
    // VERBOSE: logs full headers and request/response bodies
    logLevel = InsforgeLogLevel.DEBUG

    install(Auth)
    install(Database)
    // ... other modules
}
Log LevelDescription
NONENo logging (default, recommended for production)
ERROROnly errors
WARNWarnings and errors
INFOInformational messages
DEBUGDebug info (request method, URL, response status)
VERBOSEFull details (headers, request/response bodies)
Use NONE or ERROR in production to avoid exposing sensitive data in logs.

Android Initialization

  1. Initialize InsForge SDK (With Local Storage and Browser Launcher for OAuth)
import android.content.Context
import android.content.Intent
import android.net.Uri
import dev.insforge.createInsforgeClient
import dev.insforge.ai.AI
import dev.insforge.auth.Auth
import dev.insforge.database.Database
import dev.insforge.functions.Functions
import dev.insforge.realtime.Realtime
import dev.insforge.storage.Storage
import dev.insforge.auth.BrowserLauncher
import dev.insforge.auth.SessionStorage

class InsforgeManager(private val context: Context) {
    
    val client = createInsforgeClient(
        baseUrl = "https://your-app.insforge.app",
        anonKey = "your-anon-key"
    ) {
        install(Auth) {
            // 1. config BrowserLauncher (for OAuth)
            browserLauncher = BrowserLauncher { url ->
                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                context.startActivity(intent)
            }
            
            // 2. enable session persistence
            persistSession = true
            
            // 3. config SessionStorage (use SharedPreferences)
            sessionStorage = object : SessionStorage {
                private val prefs = context.getSharedPreferences(
                    "insforge_auth", 
                    Context.MODE_PRIVATE
                )
                
                override suspend fun save(key: String, value: String) {
                    prefs.edit().putString(key, value).apply()
                }
                
                override suspend fun get(key: String): String? {
                    return prefs.getString(key, null)
                }
                
                override suspend fun remove(key: String) {
                    prefs.edit().remove(key).apply()
                }
            }
        }
        // Install Database module
        install(Database)

        // Install Realtime module for real-time subscriptions
        install(Realtime) {
            debug = true
        }
        // Install other modules
        install(Storage)
        install(Functions)
        install(AI)
    }
}
  1. Use Jetpack DataStore for Session Storage (Optional)
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map

val Context.authDataStore: DataStore<Preferences> by preferencesDataStore(name = "insforge_auth")

class DataStoreSessionStorage(private val context: Context) : SessionStorage {
    
    override suspend fun save(key: String, value: String) {
        context.authDataStore.edit { prefs ->
            prefs[stringPreferencesKey(key)] = value
        }
    }
    
    override suspend fun get(key: String): String? {
        return context.authDataStore.data.map { prefs ->
            prefs[stringPreferencesKey(key)]
        }.first()
    }
    
    override suspend fun remove(key: String) {
        context.authDataStore.edit { prefs ->
            prefs.remove(stringPreferencesKey(key))
        }
    }
}

// Then use it in your InsForge client
install(Auth) {
    browserLauncher = ...
    persistSession = true
    sessionStorage = DataStoreSessionStorage(context)
}

listModels()

List all available AI models.

Example

val models = client.ai.listModels()

models.forEach { model ->
    println("${model.provider}/${model.modelId}")
    println("  Input: ${model.inputModality}, Output: ${model.outputModality}")
    println("  Max tokens: ${model.maxTokens}")
}

chatCompletion()

Create an AI chat completion.

Parameters

  • model (String) - Model identifier (e.g., “anthropic/claude-3.5-haiku”)
  • messages (List<ChatMessage>) - Conversation messages
  • temperature (Double?, optional) - Sampling temperature (0.0-2.0)
  • maxTokens (Int?, optional) - Maximum tokens to generate
  • systemPrompt (String?, optional) - System prompt
  • webSearch (WebSearchPlugin?, optional) - Enable web search capabilities
  • fileParser (FileParserPlugin?, optional) - Enable file/PDF parsing
  • thinking (Boolean?, optional) - Enable extended reasoning mode

Returns

ChatCompletionResponse

Example (Basic)

val response = client.ai.chatCompletion(
    model = "anthropic/claude-3.5-haiku",
    messages = listOf(
        ChatMessage(role = "user", content = "What is the capital of France?")
    )
)

println(response.text)  // Direct access to text content
println("Tokens used: ${response.metadata.usage.totalTokens}")

// Access annotations if available
response.annotations?.forEach { annotation ->
    println("Citation: ${annotation.urlCitation.url}")
}

Example (With Parameters)

val response = client.ai.chatCompletion(
    model = "openai/gpt-4",
    messages = listOf(
        ChatMessage(role = "system", content = "You are a helpful assistant."),
        ChatMessage(role = "user", content = "Explain quantum computing in simple terms")
    ),
    temperature = 0.7,
    maxTokens = 1000
)

println(response.text)

Example (Multi-turn Conversation)

val conversationHistory = mutableListOf<ChatMessage>()

// First message
conversationHistory.add(ChatMessage(role = "user", content = "What is Kotlin?"))

val response1 = client.ai.chatCompletion(
    model = "anthropic/claude-3.5-haiku",
    messages = conversationHistory
)

conversationHistory.add(ChatMessage(role = "assistant", content = response1.text))

// Follow-up
conversationHistory.add(ChatMessage(role = "user", content = "What are its main features?"))

val response2 = client.ai.chatCompletion(
    model = "anthropic/claude-3.5-haiku",
    messages = conversationHistory
)

println(response2.text)
val response = client.ai.chatCompletion(
    model = "openai/gpt-4",
    messages = listOf(
        ChatMessage(role = "user", content = "What are the latest news about AI?")
    ),
    webSearch = WebSearchPlugin(
        enabled = true,
        maxResults = 5,
        engine = WebSearchEngine.NATIVE
    )
)

println(response.text)

// Access URL citations from search results
response.annotations?.forEach { annotation ->
    println("Source: ${annotation.urlCitation.title} - ${annotation.urlCitation.url}")
}

Example (With PDF Parsing)

val response = client.ai.chatCompletion(
    model = "openai/gpt-4",
    messages = listOf(
        ChatMessage(role = "user", content = "Summarize the content of this PDF")
    ),
    fileParser = FileParserPlugin(
        enabled = true,
        pdf = PdfParserConfig(engine = PdfEngine.MISTRAL_OCR)
    )
)

println(response.text)

Example (With Extended Reasoning)

val response = client.ai.chatCompletion(
    model = "anthropic/claude-3.5-sonnet",
    messages = listOf(
        ChatMessage(role = "user", content = "Solve this complex math problem step by step...")
    ),
    thinking = true
)

println(response.text)

Example (Combined Features)

val response = client.ai.chatCompletion(
    model = "openai/gpt-4",
    messages = messages,
    webSearch = WebSearchPlugin(enabled = true, maxResults = 3),
    fileParser = FileParserPlugin(enabled = true),
    thinking = true
)

println(response.text)

chatCompletionWithWebSearch()

Convenience method for chat completion with web search enabled.

Parameters

  • model (String) - Model identifier
  • messages (List<ChatMessage>) - Conversation messages
  • maxResults (Int?, optional) - Maximum search results (default: 5)
  • engine (WebSearchEngine?, optional) - Search engine to use
  • temperature (Double?, optional) - Sampling temperature
  • maxTokens (Int?, optional) - Maximum tokens to generate

Example

val response = client.ai.chatCompletionWithWebSearch(
    model = "openai/gpt-4",
    messages = listOf(
        ChatMessage(role = "user", content = "What are today's top news headlines?")
    ),
    maxResults = 5
)

println(response.text)

// Access citations
response.annotations?.forEach { annotation ->
    println("- ${annotation.urlCitation.title}: ${annotation.urlCitation.url}")
}

chatCompletionWithThinking()

Convenience method for chat completion with extended reasoning mode enabled.

Parameters

  • model (String) - Model identifier
  • messages (List<ChatMessage>) - Conversation messages
  • temperature (Double?, optional) - Sampling temperature
  • maxTokens (Int?, optional) - Maximum tokens to generate

Example

val response = client.ai.chatCompletionWithThinking(
    model = "anthropic/claude-3.5-sonnet",
    messages = listOf(
        ChatMessage(role = "user", content = "Analyze this complex problem and provide a detailed solution...")
    )
)

println(response.text)

chatCompletionStream()

Create a streaming chat completion. Returns Flow<String> that emits content chunks directly.

Parameters

  • model (String) - Model identifier
  • messages (List<ChatMessage>) - Conversation messages
  • temperature (Double?, optional) - Sampling temperature
  • maxTokens (Int?, optional) - Maximum tokens to generate
  • webSearch (WebSearchPlugin?, optional) - Enable web search capabilities
  • fileParser (FileParserPlugin?, optional) - Enable file/PDF parsing
  • thinking (Boolean?, optional) - Enable extended reasoning mode

Example

client.ai.chatCompletionStream(
    model = "anthropic/claude-3.5-haiku",
    messages = listOf(
        ChatMessage(role = "user", content = "Tell me a story")
    )
).collect { content ->
    print(content)  // Content string directly
}

Example (With StringBuilder)

val fullResponse = StringBuilder()

client.ai.chatCompletionStream(
    model = "openai/gpt-4",
    messages = listOf(
        ChatMessage(role = "user", content = "Explain quantum computing")
    ),
    temperature = 0.7
).collect { content ->
    fullResponse.append(content)
    // Update UI with each chunk
    updateUI(fullResponse.toString())
}

println("Complete response: $fullResponse")
client.ai.chatCompletionStream(
    model = "openai/gpt-4",
    messages = listOf(
        ChatMessage(role = "user", content = "What's happening in tech news today?")
    ),
    webSearch = WebSearchPlugin(enabled = true, maxResults = 3)
).collect { content ->
    print(content)
}

generateEmbeddings()

Generate vector embeddings for text input using AI models.

Parameters

  • model (String) - Embedding model identifier (e.g., “google/gemini-embedding-001”)
  • input (String?, optional) - Single text input to embed
  • inputs (List<String>?, optional) - Multiple text inputs to embed
  • encodingFormat (EmbeddingEncodingFormat?, optional) - Output format (FLOAT or BASE64)
  • dimensions (Int?, optional) - Number of dimensions for the output embeddings

Returns

EmbeddingsResponse

Example (Single Text)

val response = client.ai.generateEmbeddings(
    model = "google/gemini-embedding-001",
    input = "Hello world"
)

println("Generated ${response.data.size} embedding(s)")
println("Dimensions: ${response.data.first().embedding.size}")
println("Model: ${response.metadata?.model}")

Example (Multiple Texts)

val response = client.ai.generateEmbeddings(
    model = "google/gemini-embedding-001",
    inputs = listOf("Hello", "World", "How are you?")
)

response.data.forEachIndexed { index, embedding ->
    println("Embedding $index: ${embedding.embedding.take(5)}...")  // First 5 dimensions
}

Example (With Optional Parameters)

val response = client.ai.generateEmbeddings(
    model = "google/gemini-embedding-001",
    input = "Hello world",
    encodingFormat = EmbeddingEncodingFormat.FLOAT,
    dimensions = 512
)

println("Embedding dimensions: ${response.data.first().embedding.size}")  // 512
// Generate embeddings for documents
val documents = listOf(
    "Kotlin is a modern programming language",
    "Android development with Jetpack Compose",
    "Machine learning with TensorFlow"
)

val docEmbeddings = client.ai.generateEmbeddings(
    model = "google/gemini-embedding-001",
    inputs = documents
)

// Generate embedding for query
val queryEmbedding = client.ai.generateEmbeddings(
    model = "google/gemini-embedding-001",
    input = "mobile app development"
)

// Calculate cosine similarity (simplified)
fun cosineSimilarity(a: List<Double>, b: List<Double>): Double {
    val dotProduct = a.zip(b).sumOf { it.first * it.second }
    val normA = sqrt(a.sumOf { it * it })
    val normB = sqrt(b.sumOf { it * it })
    return dotProduct / (normA * normB)
}

// Find most similar document
val queryVector = queryEmbedding.data.first().embedding
val similarities = docEmbeddings.data.mapIndexed { index, embedding ->
    index to cosineSimilarity(queryVector, embedding.embedding)
}

val mostSimilar = similarities.maxByOrNull { it.second }
println("Most similar: ${documents[mostSimilar!!.first]}")

generateImage()

Generate images using AI models.

Parameters

  • model (String) - Image generation model (e.g., “openai/dall-e-3”)
  • prompt (String) - Image description

Returns

ImageGenerationResponse

Example

val response = client.ai.generateImage(
    model = "google/gemini-2.5-flash-image-preview",
    prompt = "A serene mountain landscape at sunset"
)

println("Generated ${response.count} image(s)")

response.images.forEach { image ->
    val imageUrl = image.imageUrl.url

    if (imageUrl.startsWith("data:image")) {
        // Handle base64 encoded image
        val base64Data = imageUrl.substringAfter("base64,")
        val imageData = Base64.decode(base64Data, Base64.DEFAULT)
        val bitmap = BitmapFactory.decodeByteArray(imageData, 0, imageData.size)
        imageView.setImageBitmap(bitmap)
    } else {
        // Handle URL - load with Coil/Glide
        // AsyncImage(model = imageUrl, ...)
    }
}

Example (Save to Storage)

val response = client.ai.generateImage(
    model = "openai/dall-e-3",
    prompt = "A futuristic city skyline"
)

response.images.firstOrNull()?.let { image ->
    val imageUrl = image.imageUrl.url

    if (imageUrl.startsWith("data:image")) {
        val base64Data = imageUrl.substringAfter("base64,")
        val imageData = Base64.decode(base64Data, Base64.DEFAULT)

        // Upload to storage
        val uploadResult = client.storage
            .from("ai-images")
            .uploadWithAutoKey("generated.png", imageData) {
                contentType = "image/png"
            }

        // Save reference to database
        client.database
            .from("generated_images")
            .insertTyped(listOf(
                GeneratedImageRecord(
                    prompt = "A futuristic city skyline",
                    imageUrl = uploadResult.url
                )
            ))
            .returning()
            .execute<GeneratedImageRecord>()

        Log.d("AI", "Image saved: ${uploadResult.url}")
    }
}

Jetpack Compose Integration

Chat Screen

@Composable
fun ChatScreen() {
    var messages by remember { mutableStateOf<List<ChatMessage>>(emptyList()) }
    var inputText by remember { mutableStateOf("") }
    var isLoading by remember { mutableStateOf(false) }
    val scope = rememberCoroutineScope()

    Column(modifier = Modifier.fillMaxSize()) {
        LazyColumn(
            modifier = Modifier
                .weight(1f)
                .padding(16.dp),
            reverseLayout = true
        ) {
            items(messages.reversed()) { message ->
                ChatBubble(message = message)
            }
        }

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        ) {
            OutlinedTextField(
                value = inputText,
                onValueChange = { inputText = it },
                modifier = Modifier.weight(1f),
                placeholder = { Text("Message") }
            )

            Spacer(modifier = Modifier.width(8.dp))

            IconButton(
                onClick = {
                    if (inputText.isNotBlank() && !isLoading) {
                        val userMessage = ChatMessage(role = "user", content = inputText)
                        messages = messages + userMessage
                        val currentInput = inputText
                        inputText = ""

                        scope.launch {
                            isLoading = true
                            try {
                                val response = client.ai.chatCompletion(
                                    model = "anthropic/claude-3.5-haiku",
                                    messages = messages
                                )
                                messages = messages + ChatMessage(
                                    role = "assistant",
                                    content = response.text
                                )
                            } catch (e: Exception) {
                                Log.e("Chat", "Error: ${e.message}")
                            } finally {
                                isLoading = false
                            }
                        }
                    }
                },
                enabled = inputText.isNotBlank() && !isLoading
            ) {
                Icon(Icons.Default.Send, "Send")
            }
        }
    }
}

@Composable
fun ChatBubble(message: ChatMessage) {
    val isUser = message.role == "user"

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp),
        horizontalArrangement = if (isUser) Arrangement.End else Arrangement.Start
    ) {
        Card(
            colors = CardDefaults.cardColors(
                containerColor = if (isUser)
                    MaterialTheme.colorScheme.primary
                else
                    MaterialTheme.colorScheme.surfaceVariant
            )
        ) {
            Text(
                text = message.content,
                modifier = Modifier.padding(12.dp),
                color = if (isUser)
                    MaterialTheme.colorScheme.onPrimary
                else
                    MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

Streaming Chat

@Composable
fun StreamingChatScreen() {
    var response by remember { mutableStateOf("") }
    var isStreaming by remember { mutableStateOf(false) }
    var prompt by remember { mutableStateOf("") }
    val scope = rememberCoroutineScope()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        OutlinedTextField(
            value = prompt,
            onValueChange = { prompt = it },
            label = { Text("Your question") },
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(16.dp))

        Button(
            onClick = {
                scope.launch {
                    isStreaming = true
                    response = ""

                    try {
                        client.ai.chatCompletionStream(
                            model = "anthropic/claude-3.5-haiku",
                            messages = listOf(
                                ChatMessage(role = "user", content = prompt)
                            )
                        ).collect { content ->
                            response += content
                        }
                    } catch (e: Exception) {
                        Log.e("Stream", "Error: ${e.message}")
                    } finally {
                        isStreaming = false
                    }
                }
            },
            enabled = prompt.isNotBlank() && !isStreaming,
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(if (isStreaming) "Streaming..." else "Ask AI")
        }

        Spacer(modifier = Modifier.height(16.dp))

        Text(
            text = response,
            modifier = Modifier
                .weight(1f)
                .verticalScroll(rememberScrollState())
        )
    }
}

Image Generation Screen

@Composable
fun ImageGenerationScreen() {
    var prompt by remember { mutableStateOf("") }
    var generatedBitmap by remember { mutableStateOf<Bitmap?>(null) }
    var isGenerating by remember { mutableStateOf(false) }
    val scope = rememberCoroutineScope()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        OutlinedTextField(
            value = prompt,
            onValueChange = { prompt = it },
            label = { Text("Describe your image...") },
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(16.dp))

        Button(
            onClick = {
                scope.launch {
                    isGenerating = true
                    try {
                        val response = client.ai.generateImage(
                            model = "google/gemini-2.5-flash-image-preview",
                            prompt = prompt
                        )

                        response.images.firstOrNull()?.let { image ->
                            val imageUrl = image.imageUrl.url

                            if (imageUrl.startsWith("data:image")) {
                                val base64Data = imageUrl.substringAfter("base64,")
                                val data = Base64.decode(base64Data, Base64.DEFAULT)
                                generatedBitmap = BitmapFactory.decodeByteArray(
                                    data, 0, data.size
                                )
                            }
                        }
                    } catch (e: Exception) {
                        Log.e("ImageGen", "Failed: ${e.message}")
                    } finally {
                        isGenerating = false
                    }
                }
            },
            enabled = prompt.isNotBlank() && !isGenerating,
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(if (isGenerating) "Generating..." else "Generate Image")
        }

        Spacer(modifier = Modifier.height(16.dp))

        if (isGenerating) {
            CircularProgressIndicator()
        }

        generatedBitmap?.let { bitmap ->
            Image(
                bitmap = bitmap.asImageBitmap(),
                contentDescription = "Generated image",
                modifier = Modifier
                    .fillMaxWidth()
                    .height(300.dp)
            )
        }
    }
}

Error Handling

import dev.insforge.exceptions.InsforgeHttpException
import dev.insforge.exceptions.InsforgeException

try {
    val response = client.ai.chatCompletion(
        model = "anthropic/claude-3.5-haiku",
        messages = listOf(ChatMessage(role = "user", content = "Hello"))
    )
    println(response.text)
} catch (e: InsforgeHttpException) {
    when (e.error) {
        "MODEL_NOT_FOUND" -> println("Model not available")
        "RATE_LIMIT_EXCEEDED" -> println("Rate limit exceeded, try again later")
        "INVALID_REQUEST" -> println("Invalid request: ${e.message}")
        else -> println("API Error: ${e.message}")
    }
} catch (e: InsforgeException) {
    println("SDK Error: ${e.message}")
}

Models Reference

Enums

// Search engine options for web search
enum class WebSearchEngine {
    @SerialName("native") NATIVE,
    @SerialName("exa") EXA
}

// PDF processing engine options
enum class PdfEngine {
    @SerialName("pdf-text") PDF_TEXT,
    @SerialName("mistral-ocr") MISTRAL_OCR,
    @SerialName("native") NATIVE
}

Plugin Configuration

// Web search plugin configuration
@Serializable
data class WebSearchPlugin(
    val enabled: Boolean = true,
    @SerialName("max_results") val maxResults: Int? = null,
    val engine: WebSearchEngine? = null
)

// PDF parser configuration
@Serializable
data class PdfParserConfig(
    val engine: PdfEngine? = null
)

// File parser plugin configuration
@Serializable
data class FileParserPlugin(
    val enabled: Boolean = true,
    val pdf: PdfParserConfig? = null
)

ChatMessage

@Serializable
data class ChatMessage(
    val role: String,  // "user", "assistant", "system"
    val content: String
)

ChatCompletionResponse

@Serializable
data class ChatCompletionResponse(
    val success: Boolean,
    val text: String,
    val annotations: List<UrlCitationAnnotation>? = null,
    val metadata: CompletionMetadata
)

@Serializable
data class CompletionMetadata(
    val model: String,
    val usage: TokenUsage
)

@Serializable
data class TokenUsage(
    @SerialName("prompt_tokens") val promptTokens: Int,
    @SerialName("completion_tokens") val completionTokens: Int,
    @SerialName("total_tokens") val totalTokens: Int
)

Annotations

// URL citation information from web search results
@Serializable
data class UrlCitation(
    val url: String,
    val title: String? = null,
    @SerialName("start_index") val startIndex: Int? = null,
    @SerialName("end_index") val endIndex: Int? = null
)

// Annotation containing URL citation
@Serializable
data class UrlCitationAnnotation(
    val type: String,  // "url_citation"
    @SerialName("url_citation") val urlCitation: UrlCitation
)

EmbeddingsResponse

// Encoding format options for embeddings
enum class EmbeddingEncodingFormat {
    @SerialName("float") FLOAT,
    @SerialName("base64") BASE64
}

@Serializable
data class EmbeddingsResponse(
    val `object`: String,  // "list"
    val data: List\<EmbeddingObject\>,
    val metadata: EmbeddingsMetadata? = null
)

@Serializable
data class EmbeddingObject(
    val `object`: String,  // "embedding"
    val embedding: List\<Double\>,  // or String for base64 format
    val index: Int
)

@Serializable
data class EmbeddingsMetadata(
    val model: String,
    val usage: EmbeddingsUsage? = null
)

@Serializable
data class EmbeddingsUsage(
    @SerialName("prompt_tokens") val promptTokens: Int? = null,
    @SerialName("total_tokens") val totalTokens: Int? = null
)

ImageGenerationResponse

@Serializable
data class ImageGenerationResponse(
    val model: String,
    val images: List<GeneratedImage>,
    val text: String? = null,
    val count: Int,
    val metadata: ImageMetadata,
    val nextActions: String
)

@Serializable
data class GeneratedImage(
    val type: String,  // "image_url"
    @SerialName("image_url") val imageUrl: ImageUrl
)

@Serializable
data class ImageUrl(
    val url: String  // URL or data:image/... base64
)

@Serializable
data class ImageMetadata(
    val model: String,
    val provider: String
)

AIModel

@Serializable
data class AIModel(
    val id: String,
    val provider: String,
    val modelId: String,
    val inputModality: String,  // "text", "image", etc.
    val outputModality: String,
    val maxTokens: Int? = null
)