Abrarul8541 commited on
Commit
a1222b2
·
verified ·
1 Parent(s): 7f3c389

Phase 3: backends - llama.cpp (local) and cloud (OpenAI/DeepSeek/Anthropic)

Browse files
app/src/main/java/io/github/abrarul/modularai/engine/Engine.kt ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package io.github.abrarul.modularai.engine
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONObject
5
+ import java.net.HttpURLConnection
6
+ import java.net.URL
7
+ import java.nio.ByteBuffer
8
+ import java.nio.CharBuffer
9
+ import java.nio.charset.CodingErrorAction
10
+
11
+ interface Backend {
12
+ /** Streams text through [onText]; return false from it to stop. */
13
+ fun generate(prompt: String, maxTokens: Int, onText: (String) -> Boolean)
14
+ }
15
+
16
+ /** Called from native code. Keep the name and signature in sync with llama_jni.cpp and proguard. */
17
+ interface TokenSink {
18
+ fun onToken(bytes: ByteArray): Boolean
19
+ }
20
+
21
+ class LlamaEngine : Backend {
22
+ private var handle = 0L
23
+
24
+ private external fun nativeLoad(path: String, nCtx: Int, threads: Int): Long
25
+ private external fun nativeGenerate(h: Long, prompt: String, maxTokens: Int, temp: Float, sink: TokenSink)
26
+ private external fun nativeFree(h: Long)
27
+
28
+ val loaded get() = handle != 0L
29
+
30
+ fun load(path: String, nCtx: Int, threads: Int = Runtime.getRuntime().availableProcessors().coerceAtMost(6)) {
31
+ unload()
32
+ handle = nativeLoad(path, nCtx, threads)
33
+ require(handle != 0L) { "Could not load model (unsupported file, or not enough RAM)" }
34
+ }
35
+
36
+ fun unload() { if (handle != 0L) { nativeFree(handle); handle = 0L } }
37
+
38
+ override fun generate(prompt: String, maxTokens: Int, onText: (String) -> Boolean) {
39
+ check(handle != 0L) { "No model loaded" }
40
+ val dec = Charsets.UTF_8.newDecoder()
41
+ .onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE)
42
+ var pending = ByteArray(0)
43
+ nativeGenerate(handle, prompt, maxTokens, 0.7f, object : TokenSink {
44
+ override fun onToken(bytes: ByteArray): Boolean {
45
+ // A token may end mid-character; decode only complete UTF-8 sequences.
46
+ val bb = ByteBuffer.wrap(pending + bytes)
47
+ val cb = CharBuffer.allocate(bb.remaining() + 4)
48
+ dec.decode(bb, cb, false)
49
+ pending = ByteArray(bb.remaining()).also { bb.get(it) }
50
+ cb.flip()
51
+ return if (cb.isEmpty()) true else onText(cb.toString())
52
+ }
53
+ })
54
+ }
55
+
56
+ companion object { init { System.loadLibrary("modularai_llama") } }
57
+ }
58
+
59
+ /** Non-streaming cloud fallback. [provider]: openai | deepseek | anthropic. */
60
+ class CloudBackend(private val provider: String, private val key: String, private val model: String) : Backend {
61
+
62
+ override fun generate(prompt: String, maxTokens: Int, onText: (String) -> Boolean) {
63
+ val msgs = JSONArray().put(JSONObject().put("role", "user").put("content", prompt))
64
+ val anthropic = provider == "anthropic"
65
+ val url = when (provider) {
66
+ "anthropic" -> "https://api.anthropic.com/v1/messages"
67
+ "deepseek" -> "https://api.deepseek.com/chat/completions"
68
+ "openai" -> "https://api.openai.com/v1/chat/completions"
69
+ else -> error("Unknown provider: $provider (openai | deepseek | anthropic)")
70
+ }
71
+ val body = JSONObject().put("model", model).put("max_tokens", maxTokens).put("messages", msgs)
72
+ val c = URL(url).openConnection() as HttpURLConnection
73
+ c.requestMethod = "POST"
74
+ c.doOutput = true
75
+ c.connectTimeout = 20_000
76
+ c.readTimeout = 120_000
77
+ c.setRequestProperty("Content-Type", "application/json")
78
+ if (anthropic) {
79
+ c.setRequestProperty("x-api-key", key)
80
+ c.setRequestProperty("anthropic-version", "2023-06-01")
81
+ } else c.setRequestProperty("Authorization", "Bearer $key")
82
+ c.outputStream.use { it.write(body.toString().toByteArray()) }
83
+
84
+ val ok = c.responseCode in 200..299
85
+ val text = (if (ok) c.inputStream else c.errorStream).bufferedReader().readText()
86
+ if (!ok) error("HTTP ${c.responseCode}: ${text.take(300)}")
87
+ val j = JSONObject(text)
88
+ val out = if (anthropic) j.getJSONArray("content").getJSONObject(0).getString("text")
89
+ else j.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content")
90
+ onText(out)
91
+ }
92
+ }