fuzzyworld

ai-chat-android-auto / files / app / src / main / java / net / fuzzyworld / aichat / OpenAiCompatibleProvider.kt

OpenAiCompatibleProvider.kt

5.5 KB · history · blame

1package net.fuzzyworld.aichat
2
3import okhttp3.Call
4import okhttp3.Callback
5import okhttp3.MediaType.Companion.toMediaType
6import okhttp3.Request
7import okhttp3.RequestBody.Companion.toRequestBody
8import okhttp3.Response
9import org.json.JSONArray
10import org.json.JSONObject
11import java.io.IOException
12
13abstract class OpenAiCompatibleProvider : AiProvider {
14 override val supportsWebSearch = false
15 override val allowsBaseUrlOverride = false
16
17 override fun ask(
18 apiKey: String,
19 baseUrl: String,
20 model: String,
21 question: String,
22 onResult: (String?) -> Unit,
23 ) {
24 val payload = JSONObject().apply {
25 put("model", model)
26 put("max_tokens", 1024)
27 put("messages", JSONArray().apply {
28 put(JSONObject().apply {
29 put("role", "system")
30 put("content", SYSTEM_PROMPT)
31 })
32 put(JSONObject().apply {
33 put("role", "user")
34 put("content", question)
35 })
36 })
37 }.toString().toRequestBody("application/json".toMediaType())
38
39 val request = Request.Builder()
40 .url("$baseUrl/v1/chat/completions")
41 .post(payload)
42 .addHeader("Authorization", "Bearer $apiKey")
43 .build()
44
45 Http.client.newCall(request).enqueue(object : Callback {
46 override fun onFailure(call: Call, e: IOException) = onResult(null)
47 override fun onResponse(call: Call, response: Response) {
48 response.use {
49 if (!it.isSuccessful) { onResult(null); return }
50 try {
51 val text = JSONObject(it.body!!.string())
52 .getJSONArray("choices")
53 .getJSONObject(0)
54 .getJSONObject("message")
55 .getString("content")
56 onResult(text.trim().ifEmpty { null })
57 } catch (_: Exception) { onResult(null) }
58 }
59 }
60 })
61 }
62
63 override fun listModels(
64 apiKey: String,
65 baseUrl: String,
66 onResult: (List<ModelInfo>?) -> Unit,
67 ) {
68 val request = Request.Builder()
69 .url("$baseUrl/v1/models")
70 .get()
71 .addHeader("Authorization", "Bearer $apiKey")
72 .build()
73
74 Http.client.newCall(request).enqueue(object : Callback {
75 override fun onFailure(call: Call, e: IOException) = onResult(null)
76 override fun onResponse(call: Call, response: Response) {
77 response.use {
78 if (!it.isSuccessful) { onResult(null); return }
79 try {
80 val data = JSONObject(it.body!!.string()).getJSONArray("data")
81 val models = (0 until data.length())
82 .map { i -> data.getJSONObject(i) }
83 .filter { obj -> filterModel(obj) }
84 .map { obj ->
85 val mid = obj.getString("id")
86 ModelInfo(
87 id = mid,
88 displayName = obj.optString("name", mid),
89 )
90 }
91 .sortedBy { it.id }
92 onResult(models)
93 } catch (_: Exception) { onResult(null) }
94 }
95 }
96 })
97 }
98
99 /** Override to restrict which models appear in the list. */
100 open fun filterModel(obj: JSONObject): Boolean = true
101}
102
103// ── Concrete providers ────────────────────────────────────────────────────────
104
105object OpenAiProvider : OpenAiCompatibleProvider() {
106 override val id = "openai"
107 override val displayName = "OpenAI"
108 override val defaultBaseUrl = "https://api.openai.com"
109 override val defaultModel = "gpt-4o"
110 override val requiresApiKey = true
111
112 override fun filterModel(obj: JSONObject): Boolean {
113 val id = obj.optString("id", "")
114 return id.startsWith("gpt-") || id.startsWith("o1") || id.startsWith("o3") || id.startsWith("o4")
115 }
116}
117
118object GroqProvider : OpenAiCompatibleProvider() {
119 override val id = "groq"
120 override val displayName = "Groq"
121 override val defaultBaseUrl = "https://api.groq.com/openai"
122 override val defaultModel = "llama-3.3-70b-versatile"
123 override val requiresApiKey = true
124}
125
126object MistralProvider : OpenAiCompatibleProvider() {
127 override val id = "mistral"
128 override val displayName = "Mistral"
129 override val defaultBaseUrl = "https://api.mistral.ai"
130 override val defaultModel = "mistral-large-latest"
131 override val requiresApiKey = true
132}
133
134object OllamaProvider : OpenAiCompatibleProvider() {
135 override val id = "ollama"
136 override val displayName = "Ollama (local)"
137 override val defaultBaseUrl = "http://localhost:11434"
138 override val defaultModel = "llama3"
139 override val requiresApiKey = false
140 override val allowsBaseUrlOverride = true
141}
142
143object CustomOpenAiProvider : OpenAiCompatibleProvider() {
144 override val id = "custom"
145 override val displayName = "Custom (OpenAI-compatible)"
146 override val defaultBaseUrl = ""
147 override val defaultModel = ""
148 override val requiresApiKey = false
149 override val allowsBaseUrlOverride = true
150}