mistral-chat-app/app/src/main/java/com/mistral/chat/api/MistralClient.kt
2026-04-04 18:28:24 +08:00

154 lines
No EOL
5.4 KiB
Kotlin

package com.mistral.chat.api
import com.google.gson.JsonObject
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.mistral.chat.data.Message
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.concurrent.TimeUnit
class MistralClient(private val apiKey: String) {
private val client = OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build()
private val gson = Gson()
private val jsonMediaType = "application/json".toMediaType()
private var currentCall: Call? = null
companion object {
private const val BASE_URL = "https://api.mistral.ai/v1"
val AVAILABLE_MODELS = listOf(
"mistral-small-latest" to "Mistral Small",
"mistral-medium-latest" to "Mistral Medium",
"mistral-large-latest" to "Mistral Large",
"codestral-latest" to "Codestral"
)
}
suspend fun getModels(): Result<List<Pair<String, String>>> = withContext(Dispatchers.IO) {
try {
val request = Request.Builder()
.url("$BASE_URL/models")
.addHeader("Authorization", "Bearer $apiKey")
.get()
.build()
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
return@withContext Result.failure(Exception("API error: ${response.code}"))
}
val responseBody = response.body?.string() ?: ""
val responseJson = gson.fromJson(responseBody, JsonObject::class.java)
val models = responseJson
.getAsJsonArray("data")
?.mapNotNull { obj ->
val jsonObj = obj.asJsonObject
val id = jsonObj.get("id")?.asString
val created = jsonObj.get("created")?.asLong ?: 0L
if (id != null && created > 0 && id.endsWith("-latest")) {
val displayName = id
.replace("-latest", "")
.replace("-", " ")
.split(" ")
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
id to displayName
} else null
} ?: emptyList()
Result.success(models)
} catch (e: Exception) {
Result.failure(e)
}
}
fun cancelRequest() {
currentCall?.cancel()
currentCall = null
}
suspend fun chat(
model: String,
messages: List<Message>,
onChunk: ((String) -> Unit)? = null
): Result<Pair<String, String>> = withContext(Dispatchers.IO) {
try {
val jsonObject = JsonObject()
jsonObject.addProperty("model", model)
jsonObject.addProperty("temperature", 0.7)
jsonObject.addProperty("stream", onChunk != null)
val messagesArray = JsonArray()
messages.forEach { msg ->
val msgObj = JsonObject()
msgObj.addProperty("role", if (msg.isUser) "user" else "assistant")
msgObj.addProperty("content", msg.content)
messagesArray.add(msgObj)
}
jsonObject.add("messages", messagesArray)
val json = gson.toJson(jsonObject)
val body = json.toRequestBody(jsonMediaType)
val request = Request.Builder()
.url("$BASE_URL/chat/completions")
.addHeader("Authorization", "Bearer $apiKey")
.addHeader("Content-Type", "application/json")
.post(body)
.build()
currentCall = client.newCall(request)
val response = currentCall!!.execute()
if (response.code == 0 || response.code == -1) {
return@withContext Result.failure(Exception("Request cancelled"))
}
if (!response.isSuccessful) {
val errorBody = response.body?.string() ?: "Unknown error"
return@withContext Result.failure(Exception("API error: ${response.code} - $errorBody"))
}
val responseBody = response.body?.string() ?: ""
if (onChunk != null) {
onChunk(responseBody)
}
val responseJson = gson.fromJson(responseBody, JsonObject::class.java)
val choices = responseJson.getAsJsonArray("choices")
if (choices == null || choices.size() == 0) {
return@withContext Result.failure(Exception("No response from API"))
}
val content = choices
.get(0)
?.asJsonObject
?.getAsJsonObject("message")
?.get("content")
?.asString ?: ""
val usedModel = responseJson.get("model")?.asString ?: model
currentCall = null
Result.success(content to usedModel)
} catch (e: Exception) {
Result.failure(e)
}
}
}