diff --git a/src/android/app/src/main/java/com/openminis/app/ui/chat/ChatAssistantMessageUI.kt b/src/android/app/src/main/java/com/openminis/app/ui/chat/ChatAssistantMessageUI.kt
index 1e98eca9..640189b2 100644
--- a/src/android/app/src/main/java/com/openminis/app/ui/chat/ChatAssistantMessageUI.kt
+++ b/src/android/app/src/main/java/com/openminis/app/ui/chat/ChatAssistantMessageUI.kt
@@ -318,6 +318,8 @@ internal fun InlineErrorBanner(
) {
val clipboard = LocalClipboardManager.current
var localRetrying by remember(error) { mutableStateOf(false) }
+ // The provider's own words can be long; four lines would cut them off with no way to read the rest.
+ var expanded by remember(error) { mutableStateOf(false) }
val canRetry = onRetry != null && !isRetrying && !localRetrying
Row(
@@ -327,7 +329,7 @@ internal fun InlineErrorBanner(
.clip(RoundedCornerShape(12.dp))
.background(ChatColors.bad.copy(alpha = 0.12f))
.combinedClickable(
- onClick = {},
+ onClick = { expanded = !expanded },
onLongClick = {
clipboard.setText(androidx.compose.ui.text.AnnotatedString(error))
},
@@ -347,7 +349,7 @@ internal fun InlineErrorBanner(
color = ChatColors.bad,
fontSize = 12.sp,
lineHeight = 16.sp,
- maxLines = 4,
+ maxLines = if (expanded) Int.MAX_VALUE else 4,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
)
diff --git a/src/android/app/src/main/java/com/openminis/app/ui/chat/LLMErrorPresenter.kt b/src/android/app/src/main/java/com/openminis/app/ui/chat/LLMErrorPresenter.kt
index 07394643..f30381c4 100644
--- a/src/android/app/src/main/java/com/openminis/app/ui/chat/LLMErrorPresenter.kt
+++ b/src/android/app/src/main/java/com/openminis/app/ui/chat/LLMErrorPresenter.kt
@@ -12,7 +12,12 @@ import com.openminis.app.data.model.LLMError
*/
internal object LLMErrorPresenter {
- enum class Kind { AUTH, RATE_LIMIT, NETWORK, SERVER, BAD_REQUEST, NOT_FOUND, TOO_LARGE, REJECTED, DECODING, OTHER }
+ enum class Kind {
+ AUTH, RATE_LIMIT, NETWORK, SERVER, BAD_REQUEST, NOT_FOUND, TOO_LARGE, REJECTED, DECODING, OTHER,
+
+ // A 400 whose body says what was wrong. Only these five are told apart; anything else stays BAD_REQUEST.
+ BAD_TOOL_PAIRING, BAD_CONTEXT, BAD_IMAGE, BAD_MODEL, BAD_THINKING,
+ }
data class Reason(val kind: Kind, val status: Int?, val detail: String)
@@ -38,6 +43,38 @@ internal object LLMErrorPresenter {
return type?.let { TYPE_STATUS[it] }
}
+ private val TOOL_PAIRING = listOf(
+ "no tool output found", // OpenAI Responses: a function_call without its function_call_output
+ "tool_use ids were found without tool_result", // Anthropic
+ "responding to each", // Chat Completions: "...must be followed by tool messages responding to each tool_call_id"
+ "must be followed by tool messages",
+ "no tool call found", // the reverse: an output with no call
+ "function response parts", // Gemini: number of function response parts must equal function call parts
+ )
+ private val CONTEXT = listOf(
+ "context length", "context_length", "maximum context", "context window", "too many tokens",
+ "prompt is too long", "input is too long", "reduce the length", "max_tokens_to_sample",
+ )
+
+ /**
+ * What a 400 was about, read from the provider's own body. The providers word these differently, so this is a
+ * list of phrases that are specific enough not to mean something else; no match leaves the general 400 text.
+ * Pure, so the phrases are unit-tested.
+ */
+ internal fun refine400(detail: String): Kind {
+ // Providers quote identifiers differently (`tool_use`, 'tool_calls'); the phrases below carry no quotes.
+ val d = detail.lowercase().replace("`", "").replace("'", "").replace("\"", "")
+ return when {
+ TOOL_PAIRING.any { it in d } -> Kind.BAD_TOOL_PAIRING
+ CONTEXT.any { it in d } -> Kind.BAD_CONTEXT
+ "image" in d && listOf("not support", "unsupported", "invalid image", "does not accept", "cannot be", "vision", "multimodal").any { it in d } -> Kind.BAD_IMAGE
+ "model" in d && listOf("not found", "does not exist", "invalid model", "unknown model", "model_not_found", "not supported").any { it in d } -> Kind.BAD_MODEL
+ listOf("reasoning", "thinking", "budget_tokens", "effort").any { it in d } &&
+ listOf("invalid", "unsupported", "not support", "unknown", "must be", "not allowed").any { it in d } -> Kind.BAD_THINKING
+ else -> Kind.BAD_REQUEST
+ }
+ }
+
internal fun classify(e: Throwable): Reason? {
val llm = e as? LLMError ?: return null
return when (llm) {
@@ -49,7 +86,7 @@ internal object LLMErrorPresenter {
is LLMError.ProviderError -> {
val status = statusOf(llm.detail)
val kind = when (status) {
- 400 -> Kind.BAD_REQUEST
+ 400 -> refine400(llm.detail)
401, 403 -> Kind.AUTH
404 -> Kind.NOT_FOUND
413 -> Kind.TOO_LARGE
@@ -74,6 +111,11 @@ internal object LLMErrorPresenter {
Kind.NETWORK -> context.getString(R.string.llm_err_network)
Kind.SERVER -> context.getString(R.string.llm_err_server, code.ifEmpty { "5xx" })
Kind.BAD_REQUEST -> context.getString(R.string.llm_err_bad_request)
+ Kind.BAD_TOOL_PAIRING -> context.getString(R.string.llm_err_400_tool_pairing)
+ Kind.BAD_CONTEXT -> context.getString(R.string.llm_err_400_context)
+ Kind.BAD_IMAGE -> context.getString(R.string.llm_err_400_image)
+ Kind.BAD_MODEL -> context.getString(R.string.llm_err_400_model)
+ Kind.BAD_THINKING -> context.getString(R.string.llm_err_400_thinking)
Kind.NOT_FOUND -> context.getString(R.string.llm_err_not_found)
Kind.TOO_LARGE -> context.getString(R.string.llm_err_too_large)
Kind.REJECTED -> context.getString(R.string.llm_err_rejected, code)
diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml
index ec96f6a0..5dfae3de 100644
--- a/src/android/app/src/main/res/values-de/strings.xml
+++ b/src/android/app/src/main/res/values-de/strings.xml
@@ -2183,11 +2183,16 @@
Der Anbieter ist nicht erreichbar. Prüfe das Netzwerk und versuche es erneut.
Der Server des Anbieters hat ein vorübergehendes Problem (%1$s). Versuche es gleich noch einmal.
Der Anbieter hat die Anfrage abgelehnt (400). Meist liegt es an Modellname, Denkstufe, einem Anhang oder einer zu langen Unterhaltung.
+ Der Anbieter hat die Anfrage abgelehnt (400): Für einen Tool-Aufruf in diesem Chat fehlt das Ergebnis. Das liegt nicht an Modellname, Denkstufe oder Anhang. Mit einem neuen Chat geht es weiter; bei einer Meldung bitte die Anbieternachricht unten mitschicken.
+ Die Unterhaltung ist länger, als der Kontext des Modells erlaubt (400). Komprimiere sie oder starte eine neue.
+ Dieses Modell oder dieser Anbieter nimmt das Bild nicht an (400). Wechsle zu einem Modell mit Bildunterstützung oder entferne das Bild.
+ Der Anbieter kennt diesen Modellnamen nicht (400). Prüfe den Modellnamen unter Einstellungen → Anbieter.
+ Der Anbieter akzeptiert die aktuelle Denkstufe nicht (400). Senke sie oder schalte das Denken aus und versuche es erneut.
Modell oder Adresse nicht gefunden (404). Prüfe Modellname und Endpunkt unter Einstellungen → Anbieter.
Die Anfrage ist für den Anbieter zu groß (413). Kürze die Unterhaltung oder entferne große Anhänge.
Der Anbieter hat die Anfrage abgelehnt (%1$s).
Die Antwort des Anbieters konnte nicht gelesen werden. Versuche es erneut.
- Details: %1$s
+ Nachricht des Anbieters: %1$s
Komprimierung fehlgeschlagen: %1$s
Kein Modellanbieter eingerichtet. Füge unter Einstellungen → Anbieter einen hinzu.
Die Anbieterkonfiguration wurde abgelehnt.
diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml
index 3d50bdd6..ec562681 100644
--- a/src/android/app/src/main/res/values-fr/strings.xml
+++ b/src/android/app/src/main/res/values-fr/strings.xml
@@ -2079,11 +2079,16 @@
Impossible de joindre le fournisseur. Vérifiez le réseau et réessayez.
Le serveur du fournisseur rencontre un problème temporaire (%1$s). Réessayez dans un instant.
Le fournisseur a refusé la requête (400). Souvent à cause du nom du modèle, du niveau de réflexion, d\'une pièce jointe ou d\'une conversation trop longue.
+ Le fournisseur a refusé la requête (400) : un appel d\'outil de cette conversation n\'a pas de résultat. Ce n\'est pas le nom du modèle, le niveau de réflexion ni une pièce jointe. Une nouvelle conversation permet de continuer ; joignez le message du fournisseur ci-dessous si vous le signalez.
+ La conversation dépasse le contexte du modèle (400). Compressez-la ou commencez-en une nouvelle.
+ Ce modèle ou ce fournisseur n\'accepte pas l\'image (400). Passez à un modèle qui gère les images, ou retirez l\'image.
+ Le fournisseur ne reconnaît pas ce nom de modèle (400). Vérifiez le nom du modèle dans Réglages → Fournisseurs.
+ Le fournisseur n\'accepte pas le niveau de réflexion actuel (400). Baissez-le ou désactivez la réflexion, puis réessayez.
Modèle ou adresse introuvable (404). Vérifiez le nom du modèle et le point d\'accès dans Réglages → Fournisseurs.
La requête est trop volumineuse pour le fournisseur (413). Raccourcissez la conversation ou retirez les grosses pièces jointes.
Le fournisseur a rejeté la requête (%1$s).
La réponse du fournisseur est illisible. Réessayez.
- Détails : %1$s
+ Message du fournisseur : %1$s
Échec de la compaction : %1$s
Aucun fournisseur de modèle configuré. Ajoutez-en un dans Réglages → Fournisseurs.
La configuration du fournisseur a été refusée.
diff --git a/src/android/app/src/main/res/values-ja/strings.xml b/src/android/app/src/main/res/values-ja/strings.xml
index c8ba6c1d..23f59057 100644
--- a/src/android/app/src/main/res/values-ja/strings.xml
+++ b/src/android/app/src/main/res/values-ja/strings.xml
@@ -2180,11 +2180,16 @@
プロバイダーに接続できません。ネットワークを確認して再試行してください。
プロバイダーのサーバーで一時的な問題が発生しました(%1$s)。しばらくしてから再試行してください。
プロバイダーがリクエストを拒否しました(400)。モデル名、思考レベル、添付ファイル、会話が長すぎることなどが原因のことが多いです。
+ プロバイダーがリクエストを拒否しました(400):このチャットに、結果のないツール呼び出しがあります。モデル名・思考レベル・添付の問題ではありません。新しいチャットで続けられます。報告する場合は、下のプロバイダーのメッセージを添えてください。
+ 会話がモデルのコンテキスト長を超えています(400)。会話を圧縮するか、新しい会話を始めてください。
+ このモデルまたはプロバイダーは画像を受け付けません(400)。画像対応のモデルに切り替えるか、画像を外してください。
+ プロバイダーがこのモデル名を認識しません(400)。「設定 → プロバイダー」でモデル名を確認してください。
+ プロバイダーが現在の思考レベルを受け付けません(400)。下げるかオフにして再試行してください。
モデルまたはアドレスが見つかりません(404)。「設定 → プロバイダー」でモデル名とエンドポイントを確認してください。
リクエストがプロバイダーの上限を超えています(413)。会話を短くするか、大きな添付ファイルを外してください。
プロバイダーがリクエストを拒否しました(%1$s)。
プロバイダーの応答を読み取れませんでした。再試行してください。
- 詳細:%1$s
+ プロバイダーのメッセージ:%1$s
会話の圧縮に失敗しました:%1$s
モデルプロバイダーが未設定です。「設定 → プロバイダー」で追加してください。
プロバイダーの設定が拒否されました。
diff --git a/src/android/app/src/main/res/values-ko/strings.xml b/src/android/app/src/main/res/values-ko/strings.xml
index 817054d8..486e88ab 100644
--- a/src/android/app/src/main/res/values-ko/strings.xml
+++ b/src/android/app/src/main/res/values-ko/strings.xml
@@ -2172,11 +2172,16 @@
제공업체에 연결할 수 없습니다. 네트워크를 확인한 뒤 다시 시도하세요.
제공업체 서버에 일시적인 문제가 있습니다(%1$s). 잠시 후 다시 시도하세요.
제공업체가 요청을 거부했습니다(400). 모델 이름, 사고 수준, 첨부 파일, 너무 긴 대화 등이 원인인 경우가 많습니다.
+ 제공업체가 요청을 거부했습니다(400): 이 대화에 결과가 없는 도구 호출이 있습니다. 모델 이름, 사고 수준, 첨부 파일의 문제가 아닙니다. 새 대화를 시작하면 계속할 수 있습니다. 신고할 때는 아래 제공업체의 원문을 함께 보내 주세요.
+ 대화가 모델의 컨텍스트 길이를 넘었습니다(400). 대화를 압축하거나 새로 시작하세요.
+ 이 모델이나 제공업체는 이미지를 받지 않습니다(400). 이미지를 지원하는 모델로 바꾸거나 이미지를 빼세요.
+ 제공업체가 이 모델 이름을 인식하지 못합니다(400). 설정 → 제공업체에서 모델 이름을 확인하세요.
+ 제공업체가 현재 사고 수준을 받지 않습니다(400). 낮추거나 끄고 다시 시도하세요.
모델 또는 주소를 찾을 수 없습니다(404). 「설정 → 제공업체」에서 모델 이름과 엔드포인트를 확인하세요.
요청이 제공업체 한도를 넘었습니다(413). 대화를 줄이거나 큰 첨부 파일을 빼 주세요.
제공업체가 요청을 거부했습니다(%1$s).
제공업체의 응답을 읽을 수 없습니다. 다시 시도하세요.
- 자세히: %1$s
+ 제공업체 원문: %1$s
대화 압축 실패: %1$s
모델 제공업체가 설정되지 않았습니다. 「설정 → 제공업체」에서 추가하세요.
제공업체 설정이 거부되었습니다.
diff --git a/src/android/app/src/main/res/values-ru/strings.xml b/src/android/app/src/main/res/values-ru/strings.xml
index 424719d1..2d497a6f 100644
--- a/src/android/app/src/main/res/values-ru/strings.xml
+++ b/src/android/app/src/main/res/values-ru/strings.xml
@@ -2359,11 +2359,16 @@
Не удаётся связаться с провайдером. Проверьте сеть и повторите попытку.
На сервере провайдера временная неполадка (%1$s). Повторите чуть позже.
Провайдер отклонил запрос (400). Чаще всего причина в названии модели, уровне размышления, вложении или слишком длинной беседе.
+ Провайдер отклонил запрос (400): у вызова инструмента в этом чате нет результата. Дело не в имени модели, уровне размышления или вложении. Начните новый чат; если сообщаете об ошибке, приложите сообщение провайдера ниже.
+ Разговор длиннее, чем допускает контекст модели (400). Сожмите его или начните новый.
+ Эта модель или провайдер не принимает изображение (400). Выберите модель с поддержкой изображений или уберите изображение.
+ Провайдер не знает это имя модели (400). Проверьте имя модели в разделе «Настройки → Провайдеры».
+ Провайдер не принимает текущий уровень размышления (400). Понизьте его или отключите размышление и повторите.
Модель или адрес не найдены (404). Проверьте название модели и адрес в Настройки → Провайдеры.
Запрос слишком велик для провайдера (413). Сократите беседу или уберите большие вложения.
Провайдер отклонил запрос (%1$s).
Не удалось прочитать ответ провайдера. Повторите попытку.
- Подробности: %1$s
+ Сообщение провайдера: %1$s
Не удалось сжать беседу: %1$s
Провайдер модели не настроен. Добавьте его в Настройки → Провайдеры.
Настройка провайдера отклонена.
diff --git a/src/android/app/src/main/res/values-zh-rTW/strings.xml b/src/android/app/src/main/res/values-zh-rTW/strings.xml
index 91647b1c..1beabe04 100644
--- a/src/android/app/src/main/res/values-zh-rTW/strings.xml
+++ b/src/android/app/src/main/res/values-zh-rTW/strings.xml
@@ -2351,11 +2351,16 @@
連不上服務商。請檢查網路後重試。
服務商的伺服器暫時出了問題(%1$s)。請稍後重試。
服務商拒絕了這次請求(400)。通常是它不接受的內容:模型名稱、思考檔位、某個附件,或對話太長。
+ 服務商拒絕了這次請求(400):這個對話裡有一次工具呼叫沒有對應的結果。不是模型名稱、思考檔位或附件的問題。新開一個對話可以繼續;要回報的話請附上下面服務商的原文。
+ 對話超過了模型能處理的長度(400)。請壓縮對話,或新開一個。
+ 這個模型或服務商不接受圖片(400)。請換一個支援圖片的模型,或移除圖片。
+ 服務商不認得這個模型名稱(400)。請到「設定 → 服務商」檢查模型名稱。
+ 服務商不接受目前的思考檔位(400)。請調低或關閉思考後重試。
找不到模型或位址(404)。請到「設定 → 服務商」檢查模型名稱和介面位址。
請求太大,服務商無法接收(413)。請縮短對話或移除大附件。
服務商拒絕了這次請求(%1$s)。
無法讀取服務商的回覆,請重試。
- 詳情:%1$s
+ 服務商原文:%1$s
壓縮對話失敗:%1$s
尚未設定模型服務商。請到「設定 → 服務商」新增。
服務商設定被拒絕。
diff --git a/src/android/app/src/main/res/values-zh/strings.xml b/src/android/app/src/main/res/values-zh/strings.xml
index 05e2225d..8523c051 100644
--- a/src/android/app/src/main/res/values-zh/strings.xml
+++ b/src/android/app/src/main/res/values-zh/strings.xml
@@ -2375,11 +2375,16 @@
连不上服务商。请检查网络后重试。
服务商的服务器暂时出了问题(%1$s)。请稍后重试。
服务商拒绝了这次请求(400)。通常是它不接受的内容:模型名、思考档位、某个附件,或对话太长。
+ 服务商拒绝了这次请求(400):这个对话里有一次工具调用没有对应的结果。不是模型名、思考档位或附件的问题。新开一个对话可以继续;要反馈的话请带上下面服务商的原文。
+ 对话超过了模型能处理的长度(400)。请压缩对话,或新开一个。
+ 这个模型或服务商不接受图片(400)。请换一个支持图片的模型,或去掉图片。
+ 服务商不认识这个模型名(400)。请到「设置 → 服务商」检查模型名。
+ 服务商不接受当前的思考档位(400)。请调低或关闭思考后重试。
找不到模型或地址(404)。请到「设置 → 服务商」检查模型名和接口地址。
请求太大,服务商接收不了(413)。请缩短对话或去掉大附件。
服务商拒绝了这次请求(%1$s)。
无法读取服务商的回复,请重试。
- 详情:%1$s
+ 服务商原文:%1$s
压缩对话失败:%1$s
还没有配置模型服务商。请到「设置 → 服务商」添加。
服务商配置被拒绝。
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index bbe36102..9155b46b 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -2465,11 +2465,16 @@
Could not reach the provider. Check your network and try again.
The provider\'s server had a temporary problem (%1$s). Try again in a moment.
The provider refused the request (400). Usually something it does not accept: the model name, the thinking level, an attachment, or a conversation that is too long.
+ The provider refused the request (400): a tool call in this chat has no matching result. This is not your model name, thinking level or attachment. Start a new chat to continue, and keep the provider\'s message below if you report it.
+ The conversation is longer than the model\'s context allows (400). Compact the conversation or start a new one.
+ This model or provider does not accept the image (400). Switch to a model that supports images, or remove the image.
+ The provider does not recognise this model name (400). Check the model name under Settings → Providers.
+ The provider does not accept the current thinking level (400). Lower it or turn thinking off, then retry.
Model or address not found (404). Check the model name and endpoint under Settings → Providers.
The request is too large for the provider (413). Shorten the conversation or remove large attachments.
The provider rejected the request (%1$s).
The provider\'s reply could not be read. Try again.
- Details: %1$s
+ Provider\'s message: %1$s
Compaction failed: %1$s
No model provider is set up. Add one in Settings → Providers.
The provider configuration was rejected.
diff --git a/src/android/app/src/test/java/com/openminis/app/ui/chat/LLMErrorPresenterTest.kt b/src/android/app/src/test/java/com/openminis/app/ui/chat/LLMErrorPresenterTest.kt
index ac9bedb1..aee021c1 100644
--- a/src/android/app/src/test/java/com/openminis/app/ui/chat/LLMErrorPresenterTest.kt
+++ b/src/android/app/src/test/java/com/openminis/app/ui/chat/LLMErrorPresenterTest.kt
@@ -42,4 +42,44 @@ class LLMErrorPresenterTest {
fun anErrorThatIsNotAnLLMErrorIsLeftAlone() {
assertNull(LLMErrorPresenter.classify(IllegalStateException("tool failed")))
}
+
+ private fun bad(body: String) = kind(LLMError.ProviderError("[400] $body"))
+
+ @Test
+ fun aMissingToolResultIsToldApartFromTheGeneral400() {
+ assertEquals(Kind.BAD_TOOL_PAIRING, bad("No tool output found for function call call_abc123."))
+ assertEquals(
+ Kind.BAD_TOOL_PAIRING,
+ bad("An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'."),
+ )
+ assertEquals(Kind.BAD_TOOL_PAIRING, bad("messages.3: `tool_use` ids were found without `tool_result` blocks immediately after"))
+ }
+
+ @Test
+ fun otherCommon400sAreToldApart() {
+ assertEquals(Kind.BAD_CONTEXT, bad("This model's maximum context length is 131072 tokens"))
+ assertEquals(Kind.BAD_CONTEXT, bad("prompt is too long: 250000 tokens > 200000 maximum"))
+ assertEquals(Kind.BAD_IMAGE, bad("image input is not supported by this model"))
+ assertEquals(Kind.BAD_IMAGE, bad("Invalid image data"))
+ assertEquals(Kind.BAD_MODEL, bad("The model `gpt-9` does not exist"))
+ assertEquals(Kind.BAD_THINKING, bad("Invalid value for reasoning_effort"))
+ assertEquals(Kind.BAD_THINKING, bad("thinking.budget_tokens: must be >= 1024"))
+ }
+
+ @Test
+ fun a400ThatOnlyLooksLikeOneOfThemStaysGeneral() {
+ // A too-long tool_call_id is a different fault from a missing result.
+ assertEquals(Kind.BAD_REQUEST, bad("Invalid 'messages[3].tool_call_id': string too long. Expected a string with maximum length 64"))
+ assertEquals(Kind.BAD_REQUEST, bad("messages: text content blocks must be non-empty"))
+ assertEquals(Kind.BAD_REQUEST, bad(""))
+ }
+
+ @Test
+ fun onlyA400IsRefined() {
+ // The same words under another status keep that status's meaning.
+ assertEquals(Kind.TOO_LARGE, kind(LLMError.ProviderError("HTTP 413: maximum context length exceeded")))
+ assertEquals(Kind.NOT_FOUND, kind(LLMError.ProviderError("HTTP 404: model does not exist")))
+ assertEquals(Kind.SERVER, kind(LLMError.ProviderError("[500] No tool output found for function call x")))
+ assertEquals(Kind.REJECTED, kind(LLMError.ProviderError("[422] image input is not supported")))
+ }
}