@@ -71,6 +71,7 @@ import com.google.ai.sample.webrtc.WebRTCSender
7171import com.google.ai.sample.webrtc.SignalingClient
7272import org.webrtc.IceCandidate
7373import kotlin.math.max
74+ import kotlin.math.roundToLong
7475
7576class PhotoReasoningViewModel (
7677 application : Application ,
@@ -183,11 +184,11 @@ class PhotoReasoningViewModel(
183184 // to avoid re-executing already-executed commands
184185 private var incrementalCommandCount = 0
185186
186- // Mistral rate limiting per API key (1.1 seconds between requests with same key)
187+ // Mistral rate limiting per API key (1.5 seconds between requests with same key)
187188 private val mistralNextAllowedRequestAtMsByKey = mutableMapOf<String , Long >()
188189 private var lastMistralTokenTimeMs = 0L
189190 private var lastMistralTokenKey: String? = null
190- private val MISTRAL_MIN_INTERVAL_MS = 1100L
191+ private val MISTRAL_MIN_INTERVAL_MS = 1500L
191192
192193 // Accumulated full text during streaming for incremental command parsing
193194 private var streamingAccumulatedText = StringBuilder ()
@@ -609,6 +610,7 @@ class PhotoReasoningViewModel(
609610 val currentModel = com.google.ai.sample.GenerativeAiViewModelFactory .getCurrentModel()
610611
611612 clearStaleErrorState()
613+ stopExecutionFlag.set(false )
612614
613615 // Check for Human Expert model
614616 if (currentModel == ModelOption .HUMAN_EXPERT ) {
@@ -1139,11 +1141,36 @@ private fun reasonWithMistral(
11391141 mistralNextAllowedRequestAtMsByKey[key] = max(existing, nextAllowedAt)
11401142 }
11411143
1144+ fun markKeyCooldown (key : String , referenceTimeMs : Long , extraDelayMs : Long ) {
1145+ val normalizedExtraDelay = extraDelayMs.coerceAtLeast(0L )
1146+ val nextAllowedAt = referenceTimeMs + max(MISTRAL_MIN_INTERVAL_MS , normalizedExtraDelay)
1147+ val existing = mistralNextAllowedRequestAtMsByKey[key] ? : 0L
1148+ mistralNextAllowedRequestAtMsByKey[key] = max(existing, nextAllowedAt)
1149+ }
1150+
11421151 fun remainingWaitForKeyMs (key : String , nowMs : Long ): Long {
11431152 val nextAllowedAt = mistralNextAllowedRequestAtMsByKey[key] ? : 0L
11441153 return (nextAllowedAt - nowMs).coerceAtLeast(0L )
11451154 }
11461155
1156+ fun parseRetryAfterMs (headerValue : String? ): Long? {
1157+ if (headerValue.isNullOrBlank()) return null
1158+ val seconds = headerValue.trim().toDoubleOrNull() ? : return null
1159+ return (seconds * 1000.0 ).roundToLong().coerceAtLeast(0L )
1160+ }
1161+
1162+ fun parseRateLimitResetDelayMs (response : okhttp3.Response , nowMs : Long ): Long? {
1163+ val resetHeader = response.header(" x-ratelimit-reset" ) ? : return null
1164+ val resetEpochSeconds = resetHeader.trim().toLongOrNull() ? : return null
1165+ val resetMs = resetEpochSeconds * 1000L
1166+ return (resetMs - nowMs).coerceAtLeast(0L )
1167+ }
1168+
1169+ fun adaptiveRetryDelayMs (failureCount : Int ): Long {
1170+ val cappedExponent = (failureCount - 1 ).coerceIn(0 , 5 )
1171+ return 1000L shl cappedExponent // 1s, 2s, 4s, 8s, 16s, 32s
1172+ }
1173+
11471174 fun isRetryableMistralFailure (code : Int ): Boolean {
11481175 return code == 429 || code >= 500
11491176 }
@@ -1153,7 +1180,7 @@ private fun reasonWithMistral(
11531180 var consecutiveFailures = 0
11541181 var blockedKeysThisRound = mutableSetOf<String >()
11551182
1156- val maxAttempts = availableKeys.size * 2 + 3 // Allow cycling through all keys at least twice
1183+ val maxAttempts = availableKeys.size * 4 + 8
11571184 while (response == null && consecutiveFailures < maxAttempts) {
11581185 if (stopExecutionFlag.get()) break
11591186
@@ -1175,7 +1202,10 @@ private fun reasonWithMistral(
11751202 try {
11761203 val attemptResponse = client.newCall(buildRequest(selectedKey)).execute()
11771204 val requestEndMs = System .currentTimeMillis()
1178- markKeyCooldown(selectedKey, requestEndMs)
1205+ val retryAfterMs = parseRetryAfterMs(attemptResponse.header(" Retry-After" ))
1206+ val resetDelayMs = parseRateLimitResetDelayMs(attemptResponse, requestEndMs)
1207+ val serverRequestedDelayMs = max(retryAfterMs ? : 0L , resetDelayMs ? : 0L )
1208+ markKeyCooldown(selectedKey, requestEndMs, serverRequestedDelayMs)
11791209
11801210 if (attemptResponse.isSuccessful) {
11811211 response = attemptResponse
@@ -1192,39 +1222,46 @@ private fun reasonWithMistral(
11921222 attemptResponse.close()
11931223 blockedKeysThisRound.add(selectedKey)
11941224 consecutiveFailures++
1225+ val adaptiveDelay = adaptiveRetryDelayMs(consecutiveFailures)
1226+ markKeyCooldown(
1227+ selectedKey,
1228+ requestEndMs,
1229+ max(serverRequestedDelayMs, adaptiveDelay)
1230+ )
11951231 withContext(Dispatchers .Main ) {
11961232 replaceAiMessageText(
1197- " Mistral temporär nicht verfügbar (Versuch $consecutiveFailures /$maxAttempts ). Wiederhole ..." ,
1233+ " Mistral temporär nicht verfügbar (Versuch $consecutiveFailures /$maxAttempts ). Warte auf Server-Rate-Limit und wiederhole ..." ,
11981234 isPending = true
11991235 )
12001236 }
12011237 } catch (e: IOException ) {
12021238 val requestEndMs = System .currentTimeMillis()
1203- markKeyCooldown(selectedKey, requestEndMs)
1239+ val adaptiveDelay = adaptiveRetryDelayMs(consecutiveFailures + 1 )
1240+ markKeyCooldown(selectedKey, requestEndMs, adaptiveDelay)
12041241 blockedKeysThisRound.add(selectedKey)
12051242 consecutiveFailures++
1206- if (consecutiveFailures >= 5 ) {
1207- throw IOException (" Mistral request failed after 5 attempts: ${e.message} " , e)
1243+ if (consecutiveFailures >= maxAttempts ) {
1244+ throw IOException (" Mistral request failed after $maxAttempts attempts: ${e.message} " , e)
12081245 }
12091246 withContext(Dispatchers .Main ) {
12101247 replaceAiMessageText(
1211- if (consecutiveFailures >= maxAttempts) {
1212- throw IOException ( " Mistral request failed after $maxAttempts attempts: ${e.message} " , e)
1248+ " Mistral Netzwerkfehler (Versuch $consecutiveFailures / $ maxAttempts ). Wiederhole... " ,
1249+ isPending = true
12131250 )
12141251 }
12151252 }
1216- " Mistral Netzwerkfehler (Versuch $consecutiveFailures / $maxAttempts ). Wiederhole... " ,
1253+ }
12171254
12181255 if (stopExecutionFlag.get()) {
12191256 throw IOException (" Mistral request aborted." )
12201257 }
12211258
1222- val finalResponse = response ? : throw IOException (" Mistral request failed after 5 attempts." )
1259+ val finalResponse = response ? : throw IOException (" Mistral request failed after $maxAttempts attempts." )
12231260
12241261 if (! finalResponse.isSuccessful) {
12251262 val errBody = finalResponse.body?.string()
12261263 finalResponse.close()
1227- val finalResponse = response ? : throw IOException (" Mistral request failed after $maxAttempts attempts. " )
1264+ throw IOException (" Mistral Error ${finalResponse.code} : $errBody " )
12281265 }
12291266
12301267 val body = finalResponse.body ? : throw IOException (" Empty response body from Mistral" )
0 commit comments