ai-chat-android-auto / files / app / src / main / java / net / fuzzyworld / aichat / AnthropicProvider.kt
AnthropicProvider.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 AnthropicProvider : AiProvider { |
| 14 | override val id = "anthropic" |
| 15 | override val displayName = "Anthropic" |
| 16 | override val defaultBaseUrl = "https://api.anthropic.com" |
| 17 | override val defaultModel = "claude-sonnet-4-6" |
| 18 | override val requiresApiKey = true |
| 19 | override val allowsBaseUrlOverride = false |
| 20 | override val supportsWebSearch = true |
| 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("model", model) |
| 31 | put("max_tokens", 1024) |
| 32 | put("system", SYSTEM_PROMPT) |
| 33 | put("messages", JSONArray().apply { |
| 34 | put(JSONObject().apply { |
| 35 | put("role", "user") |
| 36 | put("content", question) |
| 37 | }) |
| 38 | }) |
| 39 | put("tools", JSONArray().apply { |
| 40 | put(JSONObject().apply { |
| 41 | put("type", "web_search_20250305") |
| 42 | put("name", "web_search") |
| 43 | }) |
| 44 | }) |
| 45 | }.toString().toRequestBody("application/json".toMediaType()) |
| 46 | |
| 47 | val request = Request.Builder() |
| 48 | .url("$baseUrl/v1/messages") |
| 49 | .post(payload) |
| 50 | .addHeader("x-api-key", apiKey) |
| 51 | .addHeader("anthropic-version", "2023-06-01") |
| 52 | .addHeader("anthropic-beta", "web-search-2025-03-05") |
| 53 | .build() |
| 54 | |
| 55 | Http.client.newCall(request).enqueue(object : Callback { |
| 56 | override fun onFailure(call: Call, e: IOException) = onResult(null) |
| 57 | override fun onResponse(call: Call, response: Response) { |
| 58 | response.use { |
| 59 | if (!it.isSuccessful) { onResult(null); return } |
| 60 | try { |
| 61 | val content = JSONObject(it.body!!.string()).getJSONArray("content") |
| 62 | val text = buildString { |
| 63 | for (i in 0 until content.length()) { |
| 64 | val block = content.getJSONObject(i) |
| 65 | if (block.getString("type") == "text") append(block.getString("text")) |
| 66 | } |
| 67 | }.trim() |
| 68 | onResult(text.ifEmpty { null }) |
| 69 | } catch (_: Exception) { onResult(null) } |
| 70 | } |
| 71 | } |
| 72 | }) |
| 73 | } |
| 74 | |
| 75 | override fun listModels( |
| 76 | apiKey: String, |
| 77 | baseUrl: String, |
| 78 | onResult: (List<ModelInfo>?) -> Unit, |
| 79 | ) { |
| 80 | val request = Request.Builder() |
| 81 | .url("$baseUrl/v1/models") |
| 82 | .get() |
| 83 | .addHeader("x-api-key", apiKey) |
| 84 | .addHeader("anthropic-version", "2023-06-01") |
| 85 | .build() |
| 86 | |
| 87 | Http.client.newCall(request).enqueue(object : Callback { |
| 88 | override fun onFailure(call: Call, e: IOException) = onResult(null) |
| 89 | override fun onResponse(call: Call, response: Response) { |
| 90 | response.use { |
| 91 | if (!it.isSuccessful) { onResult(null); return } |
| 92 | try { |
| 93 | val data = JSONObject(it.body!!.string()).getJSONArray("data") |
| 94 | val models = (0 until data.length()).map { i -> |
| 95 | val obj = data.getJSONObject(i) |
| 96 | ModelInfo( |
| 97 | id = obj.getString("id"), |
| 98 | displayName = obj.optString("display_name", obj.getString("id")), |
| 99 | ) |
| 100 | } |
| 101 | onResult(models) |
| 102 | } catch (_: Exception) { onResult(null) } |
| 103 | } |
| 104 | } |
| 105 | }) |
| 106 | } |
| 107 | } |