Skip to content

Commit 04eb9d9

Browse files
feat: integrate mistral large 3 with screenshot support
1 parent 121c099 commit 04eb9d9

5 files changed

Lines changed: 332 additions & 5 deletions

File tree

app/src/main/kotlin/com/google/ai/sample/ApiKeyDialog.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ fun ApiKeyDialog(
4242
loadKeysForProvider(ApiProvider.VERCEL)
4343
loadKeysForProvider(ApiProvider.GOOGLE)
4444
loadKeysForProvider(ApiProvider.CEREBRAS)
45+
loadKeysForProvider(ApiProvider.MISTRAL)
4546
}
4647

4748
Dialog(onDismissRequest = {
@@ -67,7 +68,7 @@ fun ApiKeyDialog(
6768

6869
// Provider selection
6970
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
70-
listOf(ApiProvider.VERCEL, ApiProvider.CEREBRAS, ApiProvider.GOOGLE).forEach { provider ->
71+
listOf(ApiProvider.VERCEL, ApiProvider.CEREBRAS, ApiProvider.GOOGLE, ApiProvider.MISTRAL).forEach { provider ->
7172
FilterChip(
7273
selected = selectedProvider == provider,
7374
onClick = {
@@ -88,6 +89,7 @@ fun ApiKeyDialog(
8889
ApiProvider.GOOGLE -> "https://makersuite.google.com/app/apikey"
8990
ApiProvider.CEREBRAS -> "https://cloud.cerebras.ai/"
9091
ApiProvider.VERCEL -> "https://vercel.com/ai-gateway"
92+
ApiProvider.MISTRAL -> "https://console.mistral.ai/home?profile_dialog=api-keys"
9193
ApiProvider.HUMAN_EXPERT -> return@Button
9294
}
9395
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))

app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ enum class ApiProvider {
1616
VERCEL,
1717
GOOGLE,
1818
CEREBRAS,
19+
MISTRAL,
1920
HUMAN_EXPERT
2021
}
2122

@@ -45,6 +46,7 @@ enum class ModelOption(
4546
"https://huggingface.co/na5h13/gemma-3n-E4B-it-litert-lm/resolve/main/gemma-3n-E4B-it-int4.litertlm?download=true",
4647
"4.92 GB"
4748
),
49+
MISTRAL_LARGE_3("Mistral Large 3", "mistral-large-latest", ApiProvider.MISTRAL),
4850
HUMAN_EXPERT("Human Expert", "human-expert", ApiProvider.HUMAN_EXPERT);
4951

5052
/** Whether this model supports TopK/TopP/Temperature settings */
@@ -134,7 +136,7 @@ enum class InferenceBackend {
134136
}
135137

136138
object GenerativeAiViewModelFactory {
137-
private var currentModel: ModelOption = ModelOption.GPT_5_1_CODEX_MAX
139+
private var currentModel: ModelOption = ModelOption.MISTRAL_LARGE_3
138140
private var currentBackend: InferenceBackend = InferenceBackend.GPU
139141

140142
fun setModel(modelOption: ModelOption, context: Context? = null) {
@@ -171,11 +173,11 @@ object GenerativeAiViewModelFactory {
171173

172174
fun loadModelPreference(context: Context) {
173175
val prefs = context.getSharedPreferences("inference_prefs", Context.MODE_PRIVATE)
174-
val modelNameStr = prefs.getString("selected_model", ModelOption.GPT_5_1_CODEX_MAX.name)
176+
val modelNameStr = prefs.getString("selected_model", ModelOption.MISTRAL_LARGE_3.name)
175177
currentModel = try {
176-
ModelOption.valueOf(modelNameStr ?: ModelOption.GPT_5_1_CODEX_MAX.name)
178+
ModelOption.valueOf(modelNameStr ?: ModelOption.MISTRAL_LARGE_3.name)
177179
} catch (e: IllegalArgumentException) {
178-
ModelOption.GPT_5_1_CODEX_MAX
180+
ModelOption.MISTRAL_LARGE_3
179181
}
180182
}
181183
}

app/src/main/kotlin/com/google/ai/sample/MenuScreen.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,9 +582,13 @@ fun MenuScreen(
582582
withStyle(boldStyle) { append("Preview Models") }
583583
append(" could be deactivated by Google without being handed over to the final release.\n")
584584
append("")
585+
withStyle(boldStyle) { append("Mistral Large 3") }
586+
append(" is a multimodal model (supports screenshots) and requires an API key.\n")
587+
append("")
585588
withStyle(boldStyle) { append("GPT-oss 120b") }
586589
append(" is a pure text model.\n")
587590
append("")
591+
588592
withStyle(boldStyle) { append("Gemma 27B IT") }
589593
append(" cannot handle screenshots in the API.\n")
590594
append("• GPT models (")

app/src/main/kotlin/com/google/ai/sample/ScreenCaptureService.kt

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ class ScreenCaptureService : Service() {
281281
val result = callVercelApi(modelName, apiKey, chatHistory, inputContent)
282282
responseText = result.first
283283
errorMessage = result.second
284+
} else if (apiProvider == ApiProvider.MISTRAL) {
285+
val result = callMistralApi(modelName, apiKey, chatHistory, inputContent)
286+
responseText = result.first
287+
errorMessage = result.second
284288
} else {
285289
val generativeModel = GenerativeModel(
286290
modelName = modelName,
@@ -829,3 +833,113 @@ private suspend fun callVercelApi(modelName: String, apiKey: String, chatHistory
829833

830834
return Pair(responseText, errorMessage)
831835
}
836+
837+
// Data classes for Mistral API in Service
838+
@Serializable
839+
data class ServiceMistralRequest(
840+
val model: String,
841+
val messages: List<ServiceMistralMessage>,
842+
val max_tokens: Int = 4096,
843+
val temperature: Double = 0.7,
844+
val top_p: Double = 1.0,
845+
val stream: Boolean = false
846+
)
847+
848+
@Serializable
849+
data class ServiceMistralMessage(
850+
val role: String,
851+
val content: List<ServiceMistralContent>
852+
)
853+
854+
@Serializable
855+
@JsonClassDiscriminator("type")
856+
sealed class ServiceMistralContent
857+
858+
@Serializable
859+
@SerialName("text")
860+
data class ServiceMistralTextContent(@SerialName("text") val text: String) : ServiceMistralContent()
861+
862+
@Serializable
863+
@SerialName("image_url")
864+
data class ServiceMistralImageContent(@SerialName("image_url") val imageUrl: ServiceMistralImageUrl) : ServiceMistralContent()
865+
866+
@Serializable
867+
data class ServiceMistralImageUrl(val url: String)
868+
869+
@Serializable
870+
data class ServiceMistralResponse(
871+
val choices: List<ServiceMistralChoice>
872+
)
873+
874+
@Serializable
875+
data class ServiceMistralChoice(
876+
val message: ServiceMistralResponseMessage
877+
)
878+
879+
@Serializable
880+
data class ServiceMistralResponseMessage(
881+
val role: String,
882+
val content: String
883+
)
884+
885+
private suspend fun callMistralApi(modelName: String, apiKey: String, chatHistory: List<Content>, inputContent: Content): Pair<String?, String?> {
886+
var responseText: String? = null
887+
var errorMessage: String? = null
888+
889+
val json = Json {
890+
serializersModule = SerializersModule {
891+
polymorphic(ServiceMistralContent::class) {
892+
subclass(ServiceMistralTextContent::class, ServiceMistralTextContent.serializer())
893+
subclass(ServiceMistralImageContent::class, ServiceMistralImageContent.serializer())
894+
}
895+
}
896+
ignoreUnknownKeys = true
897+
}
898+
899+
try {
900+
val messages = (chatHistory + inputContent).map { content ->
901+
val parts = content.parts.map { part ->
902+
when (part) {
903+
is TextPart -> ServiceMistralTextContent(text = part.text)
904+
is ImagePart -> ServiceMistralImageContent(imageUrl = ServiceMistralImageUrl(url = part.image.toBase64()))
905+
else -> ServiceMistralTextContent(text = "")
906+
}
907+
}
908+
ServiceMistralMessage(role = if (content.role == "user") "user" else "assistant", content = parts)
909+
}
910+
911+
val requestBody = ServiceMistralRequest(
912+
model = modelName,
913+
messages = messages
914+
)
915+
916+
val client = OkHttpClient()
917+
val mediaType = "application/json".toMediaType()
918+
val jsonBody = json.encodeToString(ServiceMistralRequest.serializer(), requestBody)
919+
920+
val request = Request.Builder()
921+
.url("https://api.mistral.ai/v1/chat/completions")
922+
.post(jsonBody.toRequestBody(mediaType))
923+
.addHeader("Content-Type", "application/json")
924+
.addHeader("Authorization", "Bearer $apiKey")
925+
.build()
926+
927+
client.newCall(request).execute().use { response ->
928+
if (!response.isSuccessful) {
929+
errorMessage = "Unexpected code ${response.code} - ${response.body?.string()}"
930+
} else {
931+
val responseBody = response.body?.string()
932+
if (responseBody != null) {
933+
val mistralResponse = json.decodeFromString(ServiceMistralResponse.serializer(), responseBody)
934+
responseText = mistralResponse.choices.firstOrNull()?.message?.content ?: "No response from model"
935+
} else {
936+
errorMessage = "Empty response body"
937+
}
938+
}
939+
}
940+
} catch (e: Exception) {
941+
errorMessage = e.localizedMessage ?: "Mistral API call failed"
942+
}
943+
944+
return Pair(responseText, errorMessage)
945+
}

0 commit comments

Comments
 (0)