From b0dc3772905242cf04cc71d358f241b6ec31739b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 11:34:45 +0000 Subject: [PATCH] Gemma / Nano Banana compat: skip tools, hint on billing-required quotas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buildRequestBody now strips 'systemInstruction' and 'tools' for models that don't accept them (Gemma, Imagen, *-image). Gemini text models are unchanged, so function calling keeps working where supported. - modelSupportsFunctionCalling() encapsulates the heuristic (name-based) next to modelEmitsImages() so newly added image-only models inherit the right request shape automatically. - When the API returns a quota-exceeded error for an image model on the free tier, append a pointer to enable billing at console.cloud.google.com/billing — the default message is misleading (says 'quota exceeded' even though the real cause is billing-required). --- .../com/gemini/bridge/RestGeminiCore.kt | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt b/core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt index 2a48eb7..42a13a4 100644 --- a/core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt +++ b/core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt @@ -591,11 +591,17 @@ class RestGeminiCore( private fun buildRequestBody(): String { val contents = JSONArray() turns.forEach { contents.put(it) } - val body = JSONObject() - .put("systemInstruction", buildSystemInstruction()) - .put("contents", contents) - .put("tools", buildToolsJson()) - if (modelEmitsImages(model)) { + val body = JSONObject().put("contents", contents) + val emitsImages = modelEmitsImages(model) + val supportsTools = modelSupportsFunctionCalling(model) + // Gemma + image-only models reject systemInstruction on the generateContent + // endpoint ("400: system_instruction is not supported"). Gemini 2.5+ is + // fine and benefits from the workspace/tool preamble. + if (supportsTools) { + body.put("systemInstruction", buildSystemInstruction()) + body.put("tools", buildToolsJson()) + } + if (emitsImages) { body.put( "generationConfig", JSONObject().put( @@ -737,9 +743,23 @@ class RestGeminiCore( } private fun tryExtractError(body: String): String? = runCatching { - JSONObject(body).optJSONObject("error")?.optString("message") + val raw = JSONObject(body).optJSONObject("error")?.optString("message") ?: return@runCatching null + if (looksLikeImageFreeTierQuota(raw)) { + raw + "\n\n" + + "Image generation models (Imagen, Nano Banana / *-image) are " + + "not included in the Gemini free tier (limit 0). Enable " + + "billing on the Google Cloud project linked to your API key: " + + "https://console.cloud.google.com/billing — then retry." + } else raw }.getOrNull() + private fun looksLikeImageFreeTierQuota(message: String): Boolean { + val m = message.lowercase() + return m.contains("quota") && + m.contains("limit: 0") && + (m.contains("-image") || m.contains("imagen")) + } + private fun mapToJson(map: Map): JSONObject { val obj = JSONObject() map.forEach { (k, v) -> obj.put(k, toJson(v)) } @@ -808,5 +828,20 @@ class RestGeminiCore( */ fun modelEmitsImages(modelName: String): Boolean = modelName.contains("-image", ignoreCase = true) + + /** + * True when the model accepts `tools` + `systemInstruction` in the + * request body. Gemma models and image-only models do not (the API + * returns 400). Gemini text/multimodal models do. + */ + fun modelSupportsFunctionCalling(modelName: String): Boolean { + val lower = modelName.lowercase() + if (lower.startsWith("gemma")) return false + if (lower.startsWith("imagen")) return false + // `*-image*` chat models (Nano Banana etc.) reject tools today — + // they only produce images + short captions. + if (lower.contains("-image")) return false + return true + } } }