Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ dependencies {
// Api Client
implementation(platform(libs.openai.client.bom))
implementation(libs.openai.client)
implementation(libs.ktor.client.core)
runtimeOnly(libs.ktor.client.okhttp)

// Room
Expand Down
22 changes: 15 additions & 7 deletions data/src/main/java/com/beradeep/aiyo/data/Mappers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import com.aallam.openai.api.chat.ChatRole
import com.beradeep.aiyo.data.local.db.entity.ConversationEntity
import com.beradeep.aiyo.data.local.db.entity.MessageEntity
import com.beradeep.aiyo.data.local.kv.entity.ModelEntity
import com.beradeep.aiyo.data.remote.OpenRouterModelDto
import com.beradeep.aiyo.domain.model.Conversation
import com.beradeep.aiyo.domain.model.Message
import com.beradeep.aiyo.domain.model.Model
import com.beradeep.aiyo.domain.model.Role
import java.util.Date
import java.util.UUID
import com.aallam.openai.api.model.Model as OpenAIModel

fun ChatMessage.toChatMessage(): Message = Message(
role = this.role.toRole(),
Expand All @@ -32,12 +32,16 @@ fun Role.toChatRole(): ChatRole = when (this) {
Role.System -> ChatRole.System
}

fun OpenAIModel.toModel(): Model = Model(
id = this.id.id,
createdAt = this.created?.let { Date(it) },
ownedBy = this.ownedBy
fun OpenRouterModelDto.toModel(): Model = Model(
id = id,
createdAt = created?.let { Date(it * 1000) },
inputPricePerMillion = pricing?.prompt?.toPricePerMillion(),
outputPricePerMillion = pricing?.completion?.toPricePerMillion()
)

private fun String.toPricePerMillion(): Double? =
toDoubleOrNull()?.takeIf { it >= 0 }?.times(1_000_000)

fun ConversationEntity.toDomain(): Conversation = Conversation(
id = id,
title = title,
Expand Down Expand Up @@ -73,11 +77,15 @@ fun Message.toEntity(conversationId: UUID): MessageEntity = MessageEntity(
fun ModelEntity.toDomain(): Model = Model(
id = id,
createdAt = createdAt,
ownedBy = ownedBy
ownedBy = ownedBy,
inputPricePerMillion = inputPricePerMillion,
outputPricePerMillion = outputPricePerMillion
)

fun Model.toEntity(): ModelEntity = ModelEntity(
id = id,
createdAt = createdAt,
ownedBy = ownedBy
ownedBy = ownedBy,
inputPricePerMillion = inputPricePerMillion,
outputPricePerMillion = outputPricePerMillion
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ data class ModelEntity(
val id: String,
val ownedBy: String? = null,
@Serializable(with = DateSerializer::class)
val createdAt: java.util.Date? = null
val createdAt: java.util.Date? = null,
val inputPricePerMillion: Double? = null,
val outputPricePerMillion: Double? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.beradeep.aiyo.data.remote

import kotlinx.serialization.Serializable

@Serializable
data class OpenRouterModelsResponse(
val data: List<OpenRouterModelDto>
)

@Serializable
data class OpenRouterModelDto(
val id: String,
val created: Long? = null,
val pricing: OpenRouterPricingDto? = null
)

@Serializable
data class OpenRouterPricingDto(
val prompt: String? = null,
val completion: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,32 @@ import android.content.Context
import com.beradeep.aiyo.data.local.kv.KVStore
import com.beradeep.aiyo.data.local.kv.entity.ModelEntity
import com.beradeep.aiyo.data.remote.DataApiClient
import com.beradeep.aiyo.data.remote.OpenRouterModelDto
import com.beradeep.aiyo.data.remote.OpenRouterModelsResponse
import com.beradeep.aiyo.data.toDomain
import com.beradeep.aiyo.data.toEntity
import com.beradeep.aiyo.data.toModel
import com.beradeep.aiyo.domain.model.Model
import com.beradeep.aiyo.domain.repository.ModelRepository
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.json.Json

class ModelRepositoryImpl(context: Context, val apiClient: DataApiClient) : ModelRepository {
private val kvStore by lazy { KVStore.getInstance(context) }
private val json = Json { ignoreUnknownKeys = true }
private val openAi get() = apiClient.openAI
private val httpClient by lazy { HttpClient() }
private val favoriteModelIdsFlow by lazy { MutableStateFlow(loadFavoriteModelIds()) }

override suspend fun getModels(apiKey: String?): Result<List<Model>> = openAi?.let { client ->
safeCall {
client.models().map(com.aallam.openai.api.model.Model::toModel)
}
} ?: Result.failure(IllegalStateException("OpenAI client not initialized"))
override suspend fun getModels(apiKey: String?): Result<List<Model>> = safeCall {
val body = httpClient.get(MODELS_URL).bodyAsText()
json.decodeFromString<OpenRouterModelsResponse>(body)
.data
.map(OpenRouterModelDto::toModel)
}

override fun getDefaultModel(): Model {
val modelJson = kvStore.getString(KEY_DEFAULT_MODEL)
Expand All @@ -38,9 +47,30 @@ class ModelRepositoryImpl(context: Context, val apiClient: DataApiClient) : Mode
kvStore.putString(KEY_DEFAULT_MODEL, modelJson)
}

override fun getFavoriteModelIdsFlow(): Flow<Set<String>> = favoriteModelIdsFlow

override fun toggleFavoriteModel(model: Model) {
val updated = favoriteModelIdsFlow.value.toMutableSet().apply {
if (!add(model.id)) remove(model.id)
}
kvStore.putString(KEY_FAVORITE_MODELS, json.encodeToString(updated.toSet()))
favoriteModelIdsFlow.value = updated
}

private fun loadFavoriteModelIds(): Set<String> =
kvStore.getString(KEY_FAVORITE_MODELS)?.let {
try {
json.decodeFromString<Set<String>>(it)
} catch (_: Throwable) {
emptySet()
}
} ?: emptySet()

private suspend fun <T> safeCall(call: suspend () -> T): Result<T> = runCatching { call() }

companion object {
private const val KEY_DEFAULT_MODEL = "default_model"
private const val KEY_FAVORITE_MODELS = "favorite_models"
private const val MODELS_URL = DataApiClient.BASE_URL + "models"
}
}
4 changes: 3 additions & 1 deletion domain/src/main/java/com/beradeep/aiyo/domain/model/Model.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import java.util.Date
data class Model(
val id: String,
val ownedBy: String? = null,
val createdAt: Date? = null
val createdAt: Date? = null,
val inputPricePerMillion: Double? = null,
val outputPricePerMillion: Double? = null
) {
override fun equals(other: Any?): Boolean {
return this.id == (other as Model).id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.beradeep.aiyo.domain.repository

import com.beradeep.aiyo.domain.model.Model
import kotlinx.coroutines.flow.Flow

interface ModelRepository {

Expand All @@ -9,4 +10,8 @@ interface ModelRepository {
fun getDefaultModel(): Model

fun setDefaultModel(model: Model)

fun getFavoriteModelIdsFlow(): Flow<Set<String>>

fun toggleFavoriteModel(model: Model)
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-te
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
ktor-client-core = { module = "io.ktor:ktor-client-core" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" }
lumo-composables = { group = "com.nomanr", name = "composables", version.ref = "lumo-composables" }
mikepenz-multiplatform-markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "multiplatformMarkdownRendererAndroid" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,14 @@ private fun ChatScreen(
isVisible = showModelSheet,
models = uiState.models,
selectedModel = uiState.selectedModel,
favoriteModelIds = uiState.favoriteModelIds,
onModelSelected = { model ->
onUiEvent(ChatUiEvent.OnModelSelected(model))
showModelSheet = false
},
onToggleFavoriteModel = { model ->
onUiEvent(ChatUiEvent.OnToggleFavoriteModel(model))
},
isFetchingModels = uiState.isFetchingModels,
fetchModels = { onUiEvent(ChatUiEvent.OnFetchModels) },
onDismiss = { showModelSheet = false }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ sealed interface ChatUiEvent {

data class OnModelSelected(val model: Model) : ChatUiEvent

data class OnToggleFavoriteModel(val model: Model) : ChatUiEvent

data object OnFetchModels : ChatUiEvent

data object OnNewChat : ChatUiEvent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.mikepenz.markdown.model.State
data class ChatUiState(
val models: List<Model>,
val selectedModel: Model,
val favoriteModelIds: Set<String>,
val selectedConversation: Conversation?,
val streamingResponse: String?,
val isLoadingResponse: Boolean,
Expand All @@ -34,6 +35,7 @@ data class ChatUiState(
ChatUiState(
models = listOf(defaultModel),
selectedModel = defaultModel,
favoriteModelIds = emptySet(),
selectedConversation = null,
streamingResponse = null,
isLoadingResponse = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ open class ChatViewModel(
}.onStart {
loadApiKey()
observeFontSizes()
observeFavoriteModels()
loadDefaultModel()
fetchModels()
}.stateIn(
Expand All @@ -86,6 +87,8 @@ open class ChatViewModel(
is ChatUiEvent.OnCancelPreloadMarkdownJobs -> cancelExistingPreloadJobs()
is ChatUiEvent.OnFetchModels -> viewModelScope.launch { fetchModels() }
is ChatUiEvent.OnModelSelected -> viewModelScope.launch { selectModel(chatUiEvent.model) }
is ChatUiEvent.OnToggleFavoriteModel ->
modelRepository.toggleFavoriteModel(chatUiEvent.model)
is ChatUiEvent.OnConversationSelected -> viewModelScope.launch {
selectConversation(chatUiEvent.conversation)
}
Expand Down Expand Up @@ -154,6 +157,14 @@ open class ChatViewModel(
}
}

private fun observeFavoriteModels() {
viewModelScope.launch {
modelRepository.getFavoriteModelIdsFlow().collect { favoriteModelIds ->
_uiState.update { it.copy(favoriteModelIds = favoriteModelIds) }
}
}
}

private fun loadDefaultModel() {
viewModelScope.launch {
defaultModel = modelRepository.getDefaultModel()
Expand Down
Loading