@@ -70,6 +70,7 @@ import kotlinx.serialization.modules.subclass
7070import com.google.ai.sample.webrtc.WebRTCSender
7171import com.google.ai.sample.webrtc.SignalingClient
7272import org.webrtc.IceCandidate
73+ import kotlin.math.max
7374
7475class PhotoReasoningViewModel (
7576 application : Application ,
@@ -182,9 +183,11 @@ class PhotoReasoningViewModel(
182183 // to avoid re-executing already-executed commands
183184 private var incrementalCommandCount = 0
184185
185- // Mistral rate limiting: track last request time to enforce 1-second minimum interval
186- private var lastMistralRequestTimeMs = 0L
187- private val MISTRAL_MIN_INTERVAL_MS = 1000L
186+ // Mistral rate limiting per API key (1.1 seconds between requests with same key)
187+ private val mistralNextAllowedRequestAtMsByKey = mutableMapOf<String , Long >()
188+ private var lastMistralTokenTimeMs = 0L
189+ private var lastMistralTokenKey: String? = null
190+ private val MISTRAL_MIN_INTERVAL_MS = 1100L
188191
189192 // Accumulated full text during streaming for incremental command parsing
190193 private var streamingAccumulatedText = StringBuilder ()
@@ -1052,12 +1055,6 @@ private fun reasonWithMistral(
10521055 resetStreamingCommandState()
10531056
10541057 viewModelScope.launch(Dispatchers .IO ) {
1055- // Rate limiting: nur die verbleibende Zeit warten
1056- val elapsed = System .currentTimeMillis() - lastMistralRequestTimeMs
1057- if (lastMistralRequestTimeMs > 0 && elapsed < MISTRAL_MIN_INTERVAL_MS ) {
1058- delay(MISTRAL_MIN_INTERVAL_MS - elapsed)
1059- }
1060-
10611058 try {
10621059 val currentModel = com.google.ai.sample.GenerativeAiViewModelFactory .getCurrentModel()
10631060 val genSettings = com.google.ai.sample.util.GenerationSettingsPreferences .loadSettings(context, currentModel.modelName)
@@ -1126,60 +1123,133 @@ private fun reasonWithMistral(
11261123 .addHeader(" Authorization" , " Bearer $key " )
11271124 .build()
11281125
1129- var currentKey = initialApiKey
1130- var response = client.newCall(buildRequest(currentKey)).execute()
1131- lastMistralRequestTimeMs = System .currentTimeMillis()
1126+ val availableKeys = apiKeyManager.getApiKeys(ApiProvider .MISTRAL )
1127+ .filter { it.isNotBlank() }
1128+ .distinct()
1129+ if (availableKeys.isEmpty()) {
1130+ throw IOException (" Mistral API key not found." )
1131+ }
11321132
1133- if (response.code == 429 ) {
1134- response.close()
1135- apiKeyManager.markKeyAsFailed(currentKey, ApiProvider .MISTRAL )
1136- val nextKey = apiKeyManager.switchToNextAvailableKey(ApiProvider .MISTRAL )
1137- if (nextKey != null && nextKey != currentKey) {
1138- // Anderer Key verfugbar -> sofort wechseln wie bisher
1139- currentKey = nextKey
1140- val elapsed2 = System .currentTimeMillis() - lastMistralRequestTimeMs
1141- if (elapsed2 < MISTRAL_MIN_INTERVAL_MS ) delay(MISTRAL_MIN_INTERVAL_MS - elapsed2)
1142- response = client.newCall(buildRequest(currentKey)).execute()
1143- lastMistralRequestTimeMs = System .currentTimeMillis()
1144- } else {
1145- // Kein anderer Key -> 5 Sekunden lang sofort wiederholen
1146- apiKeyManager.resetFailedKeys(ApiProvider .MISTRAL )
1133+ // Validate that we have at least one key before proceeding
1134+ require(availableKeys.isNotEmpty()) { " No valid Mistral API keys available after filtering" }
1135+
1136+ fun markKeyCooldown (key : String , referenceTimeMs : Long ) {
1137+ val nextAllowedAt = referenceTimeMs + MISTRAL_MIN_INTERVAL_MS
1138+ val existing = mistralNextAllowedRequestAtMsByKey[key] ? : 0L
1139+ mistralNextAllowedRequestAtMsByKey[key] = max(existing, nextAllowedAt)
1140+ }
1141+
1142+ fun remainingWaitForKeyMs (key : String , nowMs : Long ): Long {
1143+ val nextAllowedAt = mistralNextAllowedRequestAtMsByKey[key] ? : 0L
1144+ return (nextAllowedAt - nowMs).coerceAtLeast(0L )
1145+ }
1146+
1147+ fun isRetryableMistralFailure (code : Int ): Boolean {
1148+ return code == 429 || code >= 500
1149+ }
1150+
1151+ var response: okhttp3.Response ? = null
1152+ var selectedKeyForResponse: String? = null
1153+ var consecutiveFailures = 0
1154+ var blockedKeysThisRound = mutableSetOf<String >()
1155+
1156+ val maxAttempts = availableKeys.size * 2 + 3 // Allow cycling through all keys at least twice
1157+ while (response == null && consecutiveFailures < maxAttempts) {
1158+ if (stopExecutionFlag.get()) break
1159+
1160+ val now = System .currentTimeMillis()
1161+ val keyPool = availableKeys.filter { it !in blockedKeysThisRound }.ifEmpty {
1162+ blockedKeysThisRound.clear()
1163+ availableKeys
1164+ }
1165+
1166+ val keyWithLeastWait = keyPool.minByOrNull { remainingWaitForKeyMs(it, now) } ? : availableKeys.first()
1167+ val waitMs = remainingWaitForKeyMs(keyWithLeastWait, now)
1168+ if (waitMs > 0L ) {
1169+ delay(waitMs)
1170+ }
1171+
1172+ val selectedKey = keyWithLeastWait
1173+ selectedKeyForResponse = selectedKey
1174+
1175+ try {
1176+ val attemptResponse = client.newCall(buildRequest(selectedKey)).execute()
1177+ val requestEndMs = System .currentTimeMillis()
1178+ markKeyCooldown(selectedKey, requestEndMs)
1179+
1180+ if (attemptResponse.isSuccessful) {
1181+ response = attemptResponse
1182+ break
1183+ }
1184+
1185+ val isRetryable = isRetryableMistralFailure(attemptResponse.code)
1186+ if (! isRetryable) {
1187+ val errBody = attemptResponse.body?.string()
1188+ attemptResponse.close()
1189+ throw IllegalStateException (" Mistral Error ${attemptResponse.code} : $errBody " )
1190+ }
1191+
1192+ attemptResponse.close()
1193+ blockedKeysThisRound.add(selectedKey)
1194+ consecutiveFailures++
11471195 withContext(Dispatchers .Main ) {
1148- replaceAiMessageText(" Rate limit erreicht. Wiederhole..." , isPending = true )
1196+ replaceAiMessageText(
1197+ " Mistral temporär nicht verfügbar (Versuch $consecutiveFailures /$maxAttempts ). Wiederhole..." ,
1198+ isPending = true
1199+ )
11491200 }
1150- val retryDeadline = System .currentTimeMillis() + 5000L
1151- var retryResponse: okhttp3.Response ? = null
1152- while (System .currentTimeMillis() < retryDeadline) {
1153- if (stopExecutionFlag.get()) break
1154- val retryResp = client.newCall(buildRequest(currentKey)).execute()
1155- lastMistralRequestTimeMs = System .currentTimeMillis()
1156- if (retryResp.code != 429 ) {
1157- retryResponse = retryResp
1158- break
1159- }
1160- retryResp.close()
1201+ } catch (e: IOException ) {
1202+ val requestEndMs = System .currentTimeMillis()
1203+ markKeyCooldown(selectedKey, requestEndMs)
1204+ blockedKeysThisRound.add(selectedKey)
1205+ consecutiveFailures++
1206+ if (consecutiveFailures >= 5 ) {
1207+ throw IOException (" Mistral request failed after 5 attempts: ${e.message} " , e)
11611208 }
1162- if (retryResponse == null || stopExecutionFlag.get()) {
1163- throw IOException (" Mistral rate limit: Kein Erfolg innerhalb von 5 Sekunden." )
1209+ withContext(Dispatchers .Main ) {
1210+ replaceAiMessageText(
1211+ if (consecutiveFailures >= maxAttempts) {
1212+ throw IOException (" Mistral request failed after $maxAttempts attempts: ${e.message} " , e)
1213+ )
11641214 }
1165- response = retryResponse
11661215 }
1216+ " Mistral Netzwerkfehler (Versuch $consecutiveFailures /$maxAttempts ). Wiederhole..." ,
1217+
1218+ if (stopExecutionFlag.get()) {
1219+ throw IOException (" Mistral request aborted." )
11671220 }
11681221
1169- if (! response.isSuccessful) {
1170- val errBody = response.body?.string()
1171- response.close()
1172- throw IOException (" Mistral Error ${response.code} : $errBody " )
1222+ val finalResponse = response ? : throw IOException (" Mistral request failed after 5 attempts." )
1223+
1224+ if (! finalResponse.isSuccessful) {
1225+ val errBody = finalResponse.body?.string()
1226+ finalResponse.close()
1227+ val finalResponse = response ? : throw IOException (" Mistral request failed after $maxAttempts attempts." )
11731228 }
11741229
1175- val body = response .body ? : throw IOException (" Empty response body from Mistral" )
1230+ val body = finalResponse .body ? : throw IOException (" Empty response body from Mistral" )
11761231 val aiResponseText = openAiStreamParser.parse(body) { accText ->
1232+ selectedKeyForResponse?.let { key ->
1233+ lastMistralTokenKey = key
1234+ lastMistralTokenTimeMs = System .currentTimeMillis()
1235+ markKeyCooldown(key, lastMistralTokenTimeMs)
1236+ } ? : run {
1237+ Log .w(TAG , " selectedKeyForResponse is null during streaming callback" )
1238+ }
11771239 withContext(Dispatchers .Main ) {
11781240 replaceAiMessageText(accText, isPending = true )
11791241 processCommandsIncrementally(accText)
11801242 }
11811243 }
1182- response.close()
1244+ finalResponse.close()
1245+ selectedKeyForResponse?.let { key ->
1246+ val reference = if (lastMistralTokenKey == key && lastMistralTokenTimeMs > 0L ) {
1247+ lastMistralTokenTimeMs
1248+ } else {
1249+ System .currentTimeMillis()
1250+ }
1251+ markKeyCooldown(key, reference)
1252+ }
11831253
11841254 withContext(Dispatchers .Main ) {
11851255 _uiState .value = PhotoReasoningUiState .Success (aiResponseText)
0 commit comments