diff --git a/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt b/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt index aa34ffc..e8191d3 100644 --- a/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt +++ b/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt @@ -31,6 +31,7 @@ enum class ModelOption( ) { PUTER_GLM5("GLM-5 (Puter)", "z-ai/glm-5", ApiProvider.PUTER, supportsScreenshot = false), MISTRAL_LARGE_3("Mistral Large 3", "mistral-large-latest", ApiProvider.MISTRAL), + MISTRAL_MEDIUM_3_1("Mistral Medium 3.1", "mistral-medium-latest", ApiProvider.MISTRAL), GPT_5_1_CODEX_MAX("GPT-5.1 Codex Max (Vercel)", "openai/gpt-5.1-codex-max", ApiProvider.VERCEL), GPT_5_1_CODEX_MINI("GPT-5.1 Codex Mini (Vercel)", "openai/gpt-5.1-codex-mini", ApiProvider.VERCEL), GPT_5_NANO("GPT-5 Nano (Vercel)", "openai/gpt-5-nano", ApiProvider.VERCEL), diff --git a/app/src/main/kotlin/com/google/ai/sample/ScreenCaptureApiClients.kt b/app/src/main/kotlin/com/google/ai/sample/ScreenCaptureApiClients.kt index be933e8..ec0a25b 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenCaptureApiClients.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenCaptureApiClients.kt @@ -134,7 +134,20 @@ internal suspend fun callMistralApi( .build() val keysForCoordinator = availableApiKeys.filter { it.isNotBlank() }.distinct().ifEmpty { listOf(apiKey) } - val coordinated = MistralRequestCoordinator.execute(apiKeys = keysForCoordinator, maxAttempts = maxOf(4, keysForCoordinator.size * 3)) { key -> + val minIntervalMs = if (modelName == com.google.ai.sample.ModelOption.MISTRAL_MEDIUM_3_1.modelName) 420L else 1500L + val maxAttempts = if ( + modelName == com.google.ai.sample.ModelOption.MISTRAL_LARGE_3.modelName || + modelName == com.google.ai.sample.ModelOption.MISTRAL_MEDIUM_3_1.modelName + ) { + 3 + } else { + maxOf(4, keysForCoordinator.size * 3) + } + val coordinated = MistralRequestCoordinator.execute( + apiKeys = keysForCoordinator, + maxAttempts = maxAttempts, + minIntervalMs = minIntervalMs + ) { key -> client.newCall( request.newBuilder() .header("Authorization", "Bearer $key") diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt index 955dd28..bad8b1c 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt @@ -1142,10 +1142,19 @@ class PhotoReasoningViewModel( // Validate that we have at least one key before proceeding require(availableKeys.isNotEmpty()) { "No valid Mistral API keys available after filtering" } - val maxAttempts = availableKeys.size * 4 + 8 + val mistralMinIntervalMs = when (currentModel) { + ModelOption.MISTRAL_MEDIUM_3_1 -> 420L + else -> 1500L + } + val maxAttempts = when (currentModel) { + ModelOption.MISTRAL_LARGE_3, + ModelOption.MISTRAL_MEDIUM_3_1 -> 3 + else -> availableKeys.size * 4 + 8 + } val coordinated = MistralRequestCoordinator.execute( apiKeys = availableKeys, - maxAttempts = maxAttempts + maxAttempts = maxAttempts, + minIntervalMs = mistralMinIntervalMs ) { selectedKey -> if (stopExecutionFlag.get()) { throw IOException("Mistral request aborted.") diff --git a/app/src/main/kotlin/com/google/ai/sample/network/MistralRequestCoordinator.kt b/app/src/main/kotlin/com/google/ai/sample/network/MistralRequestCoordinator.kt index 82bca65..69d9821 100644 --- a/app/src/main/kotlin/com/google/ai/sample/network/MistralRequestCoordinator.kt +++ b/app/src/main/kotlin/com/google/ai/sample/network/MistralRequestCoordinator.kt @@ -30,9 +30,10 @@ internal object MistralRequestCoordinator { private suspend fun markKeyCooldown( key: String, referenceTimeMs: Long, + minIntervalMs: Long, extraDelayMs: Long = 0L ) { - val nextAllowedAt = referenceTimeMs + max(MIN_INTERVAL_MS, extraDelayMs.coerceAtLeast(0L)) + val nextAllowedAt = referenceTimeMs + max(minIntervalMs.coerceAtLeast(0L), extraDelayMs.coerceAtLeast(0L)) cooldownMutex.withLock { val existing = nextAllowedRequestAtMsByKey[key] ?: 0L nextAllowedRequestAtMsByKey[key] = max(existing, nextAllowedAt) @@ -77,6 +78,7 @@ internal object MistralRequestCoordinator { suspend fun execute( apiKeys: List, maxAttempts: Int = apiKeys.size * 4 + 8, + minIntervalMs: Long = MIN_INTERVAL_MS, request: suspend (apiKey: String) -> Response ): MistralCoordinatedResponse { require(apiKeys.isNotEmpty()) { "No Mistral API keys provided." } @@ -120,7 +122,7 @@ internal object MistralRequestCoordinator { TAG, "[$rid] response code=${response.code}, retryAfterMs=${retryAfterMs ?: -1}, resetDelayMs=${resetDelayMs ?: -1}, appliedDelayMs=$serverRequestedDelayMs" ) - markKeyCooldown(selectedKey, requestEndMs, serverRequestedDelayMs) + markKeyCooldown(selectedKey, requestEndMs, minIntervalMs, serverRequestedDelayMs) if (response.isSuccessful || !isRetryableFailure(response.code)) { Log.d(TAG, "[$rid] returning response code=${response.code} with key=${keyFingerprint(selectedKey)}") @@ -135,7 +137,7 @@ internal object MistralRequestCoordinator { TAG, "[$rid] retryable failure code=${response.code}, consecutiveFailures=$consecutiveFailures, adaptiveDelay=$adaptiveDelay" ) - markKeyCooldown(selectedKey, requestEndMs, max(serverRequestedDelayMs, adaptiveDelay)) + markKeyCooldown(selectedKey, requestEndMs, minIntervalMs, max(serverRequestedDelayMs, adaptiveDelay)) } catch (e: Exception) { val requestEndMs = System.currentTimeMillis() blockedKeysThisRound.add(selectedKey) @@ -145,7 +147,7 @@ internal object MistralRequestCoordinator { "[$rid] exception on key=${keyFingerprint(selectedKey)}, consecutiveFailures=$consecutiveFailures: ${e.message}", e ) - markKeyCooldown(selectedKey, requestEndMs, adaptiveRetryDelayMs(consecutiveFailures)) + markKeyCooldown(selectedKey, requestEndMs, minIntervalMs, adaptiveRetryDelayMs(consecutiveFailures)) if (consecutiveFailures >= maxAttempts) throw e } }