ai-chat-android-auto / files / app / src / main / java / net / fuzzyworld / aichat / GeminiProvider.kt
GeminiProvider.kt
| 1 | package net.fuzzyworld.aichat |
| 2 | |
| 3 | import okhttp3.Call |
| 4 | import okhttp3.Callback |
| 5 | import okhttp3.MediaType.Companion.toMediaType |
| 6 | import okhttp3.Request |
| 7 | import okhttp3.RequestBody.Companion.toRequestBody |
| 8 | import okhttp3.Response |
| 9 | import org.json.JSONArray |
| 10 | import org.json.JSONObject |
| 11 | import java.io.IOException |
| 12 | |
| 13 | object GeminiProvider : AiProvider { |
| 14 | override val id = "gemini" |
| 15 | override val displayName = "Google Gemini" |
| 16 | override val defaultBaseUrl = "https://generativelanguage.googleapis.com" |
| 17 | override val defaultModel = "gemini-2.0-flash" |
| 18 | override val requiresApiKey = true |
| 19 | override val allowsBaseUrlOverride = false |
| 20 | override val supportsWebSearch = false |
| 21 | |
| 22 | override fun ask( |
| 23 | apiKey: String, |
| 24 | baseUrl: String, |
| 25 | model: String, |
| 26 | question: String, |
| 27 | onResult: (String?) -> Unit, |
| 28 | ) { |
| 29 | val payload = JSONObject().apply { |
| 30 | put("system_instruction", JSONObject().apply { |
| 31 | put("parts", JSONArray().apply { |
| 32 | put(JSONObject().apply { put("text", SYSTEM_PROMPT) }) |
| 33 | }) |
| 34 | }) |
| 35 | put("contents", JSONArray().apply { |
| 36 | put(JSONObject().apply { |
| 37 | put("role", "user") |
| 38 | put("parts", JSONArray().apply { |
| 39 | put(JSONObject().apply { put("text", question) }) |
| 40 | }) |
| 41 | }) |
| 42 | }) |
| 43 | }.toString().toRequestBody("application/json".toMediaType()) |
| 44 | |
| 45 | val request = Request.Builder() |
| 46 | .url("$baseUrl/v1beta/models/$model:generateContent?key=$apiKey") |
| 47 | .post(payload) |
| 48 | .build() |
| 49 | |
| 50 | Http.client.newCall(request).enqueue(object : Callback { |
| 51 | override fun onFailure(call: Call, e: IOException) = onResult(null) |
| 52 | override fun onResponse(call: Call, response: Response) { |
| 53 | response.use { |
| 54 | if (!it.isSuccessful) { onResult(null); return } |
| 55 | try { |
| 56 | val text = JSONObject(it.body!!.string()) |
| 57 | .getJSONArray("candidates") |
| 58 | .getJSONObject(0) |
| 59 | .getJSONObject("content") |
| 60 | .getJSONArray("parts") |
| 61 | .getJSONObject(0) |
| 62 | .getString("text") |
| 63 | onResult(text.trim().ifEmpty { null }) |
| 64 | } catch (_: Exception) { onResult(null) } |
| 65 | } |
| 66 | } |
| 67 | }) |
| 68 | } |
| 69 | |
| 70 | override fun listModels( |
| 71 | apiKey: String, |
| 72 | baseUrl: String, |
| 73 | onResult: (List<ModelInfo>?) -> Unit, |
| 74 | ) { |
| 75 | val request = Request.Builder() |
| 76 | .url("$baseUrl/v1beta/models?key=$apiKey") |
| 77 | .get() |
| 78 | .build() |
| 79 | |
| 80 | Http.client.newCall(request).enqueue(object : Callback { |
| 81 | override fun onFailure(call: Call, e: IOException) = onResult(null) |
| 82 | override fun onResponse(call: Call, response: Response) { |
| 83 | response.use { |
| 84 | if (!it.isSuccessful) { onResult(null); return } |
| 85 | try { |
| 86 | val arr = JSONObject(it.body!!.string()).getJSONArray("models") |
| 87 | val models = (0 until arr.length()) |
| 88 | .map { i -> arr.getJSONObject(i) } |
| 89 | .filter { obj -> |
| 90 | val methods = obj.optJSONArray("supportedGenerationMethods") |
| 91 | methods != null && (0 until methods.length()).any { j -> |
| 92 | methods.getString(j) == "generateContent" |
| 93 | } |
| 94 | } |
| 95 | .map { obj -> |
| 96 | // "name" is "models/gemini-2.0-flash" — strip the prefix |
| 97 | val mid = obj.getString("name").removePrefix("models/") |
| 98 | ModelInfo( |
| 99 | id = mid, |
| 100 | displayName = obj.optString("displayName", mid), |
| 101 | ) |
| 102 | } |
| 103 | onResult(models) |
| 104 | } catch (_: Exception) { onResult(null) } |
| 105 | } |
| 106 | } |
| 107 | }) |
| 108 | } |
| 109 | } |