Skip to content

Commit 679bb84

Browse files
feat(assistant): Gemini Nano backend on Android, and an on-device planning benchmark (#306)
* fix(assistant): size llama.cpp threads from cpufreq, not core count refs #270 The thread count assumed every big.LITTLE arm64 phone pairs its performance cores with exactly four efficiency cores, and derived the count as hw-4. Tensor G5 (Pixel 10) has two: 2x2.25 + 5x3.05 + 1x3.78 GHz. So a Pixel 10 ran four threads where six were available, and llama-bench (Qwen3-4B Q4_K_M, fa=1) measured what that cost: pp512 19.90 t/s at four threads against 26.22 at six, regressing again at seven and eight as soon as a little core joins. Count the performance cores instead, from each core's cpufreq ceiling, keeping those within 75% of the fastest. That gap holds across every current arm64 layout, and it reproduces the previously measured optimum on Tensor G3 (5) as well as the new one on Tensor G5 (6). An unreadable cpufreq falls back to the old count-only guess, and the [4, 6] clamp is unchanged. Decode is unaffected: it measures 6.2-6.7 t/s at every thread count on Pixel 10 because it is bandwidth-bound, so this is a prefill fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(developer-guide): note iOS CI builds are possible, just not set up Chapter 14 said iOS builds locally through Xcode because of signing and App Store submission, which reads as a platform limitation. It is not: certificates, provisioning profiles, and submission all automate on a macOS runner. The workflow just has not been written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(assistant): Gemini Nano backend via AICore on Android refs #270 The Android counterpart of the Apple Foundation Models backend: the system model, reached through AICore with the ML Kit GenAI Prompt API. It is a trimmed NativeLlmProvider rather than a port of AppleIntelligenceProvider, which is the opposite of what the symmetry suggests. Foundation Models takes a GenerationSchema built per turn, so that provider constrains the reply to the turn contract and removes the answer branch until a tool result exists. ML Kit's structured output is compile-time Kotlin codegen with no union type, so no per-tool schema can be built at runtime and no branch can be removed. This backend runs the same prompt-plus-parser, self-repair loop llama.cpp does and leans on the code-level grounding checks in agent.ts instead. Two capability facts came off a Pixel 10 and contradict the documentation, so both are probed at runtime. getTokenLimit() reports 8192 on nano-v3 where the docs say under 4000, and isSystemPromptAvailable() reports false, which means a SystemInstruction is accepted and ignored: passing one would have silently dropped the tool catalog and the install-specific facts. The system text is folded into the prompt when that probe says false. Unlike Apple's, this bridge needs a download surface, because AICore fetches the weights on request rather than shipping them with the OS. A supported phone that has not downloaded them reports notReady, which becomes a download row in Settings rather than a dead end, and the hook re-probes when it finishes so the backend becomes selectable without a restart. Two AICore rejections get their own messages instead of the generic engine failure: inference is refused unless the app is in the foreground, and each app is metered per day, and "try again" is wrong advice for both. ML Kit declares minSdk 26 against the app's 24, so the manifest overrides the library rather than raising the app floor and dropping Android 7 users for a backend needing Android 14 anyway; the plugin refuses every call below API 26 so no ML Kit class loads where the runtime could not verify it. Measured on device end to end: a data question produced a tool call, the following turn answered from the tool result, and a greeting answered without calling a tool, at roughly 1.4 to 1.8 seconds per turn against the llama.cpp path's 6.6 tokens per second on the same phone. That is three spot checks, not a score: prompt-eval has not been run against this backend, and agents/project/llm-models.md records it as unmeasured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(assistant): score the Android system model with the shared on-device eval refs #270 The prompt-eval harness reaches a backend over HTTP, and neither system model has an HTTP surface, so the eval row added for Apple Foundation Models was the only way that backend ever got a score. Gemini Nano needs the same, and needed almost no new code to get it: runFmTimeEval already took any AssistantProvider, so the Apple-only row became a parameterized one rather than a second copy. The row takes a provider factory, the backend id and a model label. The factory rather than an instance because each provider caches the context window it learned per instance, and a re-run should re-learn it. The backend id is stamped on the logged report because the point of running this on two backends is comparing the two reports afterwards, which a report that does not say which backend produced it cannot support. The first run on a Pixel 10 scored 27/52 and the number is worthless: 32 of the failures were BACKGROUND_USE_BLOCKED, because the screen had dozed and AICore counts a locked screen as background. Whole case classes read as total failures while the log showed no answer had been produced at all. The playbook now says how to spot that before trusting an on-device number, since the failure looks exactly like a model that cannot do weekdays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(assistant): model downloads and evals outlive the screen that started them refs #270 The WebLLM and llama.cpp downloads already reported through the backgroundTasks store, so they survived their settings row unmounting and kept showing progress in the app-level drawer. The two surfaces added since did not: the Gemini Nano download and both system-model eval runs held progress in component state, and the eval row aborted its run in an unmount effect. Leaving Settings mid-run threw the measurement away. On Android it did worse than that. The abort called the plugin's cancelChat, which cancels the ML Kit future, and genai-prompt 1.0.0-beta4 is compiled against kotlinx-coroutines 1.7.3, where Job.cancel$default lives in Job$DefaultImpls. The app resolves coroutines 1.10.2, which dropped that class, so ML Kit's cancellation path throws NoSuchMethodError on its own thread pool, uncaught, and takes the process down. cancelChat now abandons the call rather than cancelling it: the JS side already turns an abort into an AbortError on its own and the result is discarded, so the only cost is the second or so the model had left to run, which is not worth crashing to avoid. Both surfaces now report through backgroundTasks like the older downloads, so a run shows in the drawer, keeps counting while the user is elsewhere, and is still there when they come back. Neither task carries a cancelFn, because cancelling is the crash above and AICore owns the download with no cancel of its own. Verified on a Pixel 10: an eval at 27/52 in the row read 52% in the drawer from another screen, then 31/52 in the row on return, with the app alive throughout. That also produced the first trustworthy score for this backend, 48/52, reproduced twice; the earlier 27/52 was 32 BACKGROUND_USE_BLOCKED rejections from a screen that had dozed, not wrong answers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(assistant): drop llama.cpp from Android and rank backends per platform refs #270 Android's llama.cpp integration is gone. It never had a GPU path there, so it decoded at 6.6 tok/s while the phone's own system model answered a whole turn in about a second and a half, and it charged 76MB of native libraries plus a 2.5GB model download to be the slower option. Android's on-device backend is Gemini Nano; iOS keeps llama.cpp, where Metal makes it the better of its two on-device choices. Deleted: the JNI engine, the CPU-topology helper and its check, the CMake build, NativeLlmPlugin, and the NDK wiring in build.gradle. The shared JS provider, the plugin wrapper and the Swift engine stay, because iOS still uses every one of them. Removing the abiFilters block restores what the app packaged before llama.cpp arrived, since that commit is what added it: the APK is no longer arm64-only, so 32-bit ARM devices it had silently dropped are back in range. The accuracy hint was one sentence shown on every platform, ranking backends that platform might not have. It is now three, chosen by platform and naming the model each tier runs rather than just the runtime, because "on-device" does not tell anyone what is answering them: iOS Ollama (qwen3:8b) > Qwen3 4B Instruct on Metal > Apple Intelligence Android Ollama (qwen3:8b) > Gemini Nano web Ollama (qwen3:8b) > the selected WebGPU model The chat window's system-model note had the same problem in a worse form: it sent everyone to llama.cpp, which on Android is now advice that cannot be taken. Both the current backend and the suggested one are interpolated, so iOS still points at the on-device model and Android points at the user's own Ollama server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(assistant): score the tool contract on-device, for both system models refs #270 The time eval measured what a backend does with "last tuesday night". It never touched the thing the backend ranking actually claims to rank: given a question and the tool catalog, does it pick the right tool with the right arguments. That was scored only by prompt-eval.mts, which talks HTTP and so reaches Ollama alone, so neither Apple Foundation Models nor Gemini Nano had ever been measured on it. The cases move out of prompt-eval.mts into contract-eval-cases.ts and both harnesses import them, for the reason time-eval-cases.ts already exists: a second copy of an eval drifts from the first and then measures a bug the app does not have. prompt-eval.mts keeps its scoring; only the list moved. runContractEval drives provider.chat with the production buildSystemPrompt, so each backend is scored through its real path rather than a reimplementation of one. It passes a recording runTool, which is load-bearing: Apple owns its tool loop and only reveals its calls by executing them, so without the callback the run would score Apple's fallback path instead of the one production uses, and score it as making no tool calls at all. Backends that return toolCalls ignore the callback, and both sources are read. The settings row now runs both stages under one task and one progress bar, and the report line carries each stage's breakdown alongside the combined score. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(assistant): tell AICore's rate limit apart from the plugin's busy guard refs #270 The first contract eval on a Pixel 10 reported 0/14 with every case reading "a chat is already running". Nothing was: AICore meters requests over a short window, and the contract stage started the moment the time stage's 52 calls finished, so the platform rejected all fourteen. The plugin mapped ML Kit's ErrorCode.BUSY onto the same CHAT_BUSY code as its own concurrency guard, which made a rate limit indistinguishable from a real conflict and a paced run indistinguishable from a model that got everything wrong. BUSY is RATE_LIMITED now, with its own message, and the provider tags the error it throws so a batch caller can act on it. The contract eval backs off twice and retries rather than scoring a rejection as a wrong answer, and reports how many retries a run needed, so a score that was paced by the platform says so. With that in place the eval produced the first tool-contract number any system model has had: Gemini Nano 12/14, after 11 rate-limited retries. Both failures are the same fault, answering a data question without calling a tool. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(assistant): measure answer quality on-device, the fault the other stages miss refs #270 The on-device eval scored two of the HTTP harness's six stages: can a backend read "last tuesday night", and does it fetch the right thing. Neither asks whether the answer it then writes is true, which is the class of failure this assistant has actually shipped and the one a system model is most likely to commit. A backend can pick the right tool with the right arguments and still tell the user there were fifteen events when the result carried five. Every check is a fault seen live: counts invented rather than read, monitor names invented, the data denied while rows sat in the result, the raw tool payload handed back as prose, and rows conjured over an empty result. grounding.ts catches two of them at runtime; nothing measured how often they happen. The cases and their fixtures move out of prompt-eval.mts into answer-eval-cases.ts and both harnesses import them, as the tool and time cases already do. The runner stages the question, the model's own tool call and the tool's result into the history, so the only thing left to produce is prose, and passes no runTool: a backend with a native tool loop has nothing left to call. A full on-device run is now 70 cases across three stages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(assistant): make the on-device benchmark measure planning, not prose refs #270 What the assistant has to get right is reading a request and deciding what to do about it: which tool, over which window, with which labels or tags, or that no lookup is needed at all. The benchmark now measures that and only that. The tool-plan cases go from 16 to 36. New coverage: rolling versus calendar windows (count_events cannot express "yesterday", list_events cannot express "the past hour"), clock ranges, ordinals, open-ended spans, a named camera, tags, the group and monitor surfaces, and the object vocabulary that must appear on "were there any cars" and must NOT appear on a plain summary. The eight no-tool cases are now SCORED rather than skipped. Recognising that "thanks", "who won the world cup", "how do I add a camera" and "arm the backyard camera" need no lookup is half the judgement, and skipping them measured only the half the model finds easier. Triage intercepts most of them in production, which is a safety net in front of that judgement, not a reason to leave it unmeasured. The answer-quality stage added earlier is removed with its cases and tests. It scored prose written over a canned result, which is a different axis from planning; keeping it in the run would have diluted the number this benchmark is for. One revert brings it back if the prose axis is wanted separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(assistant): give apple the full prompt on the turn that plans refs #270 Apple Foundation Models scored 21/38 on the tool-planning benchmark against Gemini Nano's 33/38, and its failures were not malformed output: the constrained decoder produced a valid call every time. They were the wrong calls. Four chose count_events for a calendar window, six defaulted to when "yesterday" whatever was asked, and four went looking through events to answer questions about the world cup and the capital of France. Those are the faults a trimmed prompt predicts. The rolling-versus-calendar distinction lives past the first sentence of two tool descriptions, and the rule about copying the person's own time words lives in the body of the shared prompt. Both were removed to fit the window. The window forces that trim only when a tool result has to fit beside the prompt. The untrimmed prompt is ~2766 tokens against 3072 usable, so on the turn that is still deciding what to fetch there is room for it, and only after a result comes back is there not. The prompt now follows that: whole shared prompt and whole tool descriptions while planning, trimmed once a successful result is in the thread. The same predicate already decided whether the schema offers an answer branch, so it is one notion of "grounded yet" rather than two. On the native tool-loop path the full descriptions go into toolsJson, where the framework already injects them, rather than into a catalog in the prompt. A catalog there would be the second, drifting copy the trimmed design was right to avoid. A tool-less turn keeps the trimmed prompt: it has nothing to choose between, and the full one would teach tool conduct to a turn with no tools. Unmeasured as yet. The benchmark that produced 21/38 is unchanged, so the re-run is a clean before/after, and Gemini Nano's 33/38 stands as the baseline because nothing shared moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(assistant): narrow the apple planning prompt to the tool descriptions refs #270 Handing Apple the whole shared system prompt on the planning turn was measured and rejected: planning moved 21/38 to 23/38, inside this model's own run-to-run spread, while failures appeared that had not been there before. Arguments leaked between cases (a German window on two English questions), objectType filled with junk ("NO", a tag name), and the no-tool cases got worse rather than better. That is a model pushed nearer its context limit, not one that was short of instructions. What did work in that run is kept: the four failures that chose count_events for a calendar window went to one, because the rolling-versus-calendar distinction lives past the first sentence of those two descriptions. So the descriptions grow on the planning turn and the system prompt stays trimmed to its dynamic facts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * revert(assistant): apple's planning gap is not the trimmed prompt refs #270 Reverts 8ec4210 and d864597. Apple Foundation Models plans 21/38 on the tool-planning benchmark against Gemini Nano's 33/38, so the trimmed prompt was the obvious suspect: the sentences it removes are exactly the ones that distinguish count_events from list_events and that tell the model to copy the person's own time words. It was measured and it was wrong. Handing Apple the whole shared system prompt plus whole tool descriptions on the planning turn moved it to 23/38, two cases on a 38-case set and inside this model's own run-to-run spread, while introducing failures that had not been there: a German window on two English questions, and objectType filled with "NO" and with a tag name. That is a model pushed nearer its context limit, not one that was short of instructions. The narrower variant, whole descriptions with the system prompt still trimmed, never got a clean run. Neither is worth the complexity of a second prompt shape and a mode switch for a move that small, so both go back out. The playbook keeps what was learned, including the one real effect: the extra text took count_events chosen for a calendar window from four failures to one. Also records the measured comparison and the screen-lock trap that voids an on-device run on both platforms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(assistant): say which backend to pick, per platform, with the numbers refs #270 The per-platform ranking was in one block near the top and nowhere else, so a reader who arrived at the Apple Intelligence or Gemini Nano section from the table of contents got no steer at all. Each of those sections now opens its closing with what to pick and why, and the accuracy chapter carries the two system models it never covered. The numbers are the ones measured on the phones: Gemini Nano planned the right lookup for 33 of 38 questions and knew when no lookup was needed 8 times out of 8; Apple Intelligence managed 21 and 4. The second column is called out, because searching a person's cameras to answer "who won the world cup in 2018" is the failure they would actually notice. Two accuracy claims are marked as not directly comparable rather than left to imply a head-to-head that was never run: the Ollama table was measured on the fourteen-question suite as it stood in July, and the system models on the thirty-eight-question one it has since grown into. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 8e0f640 commit 679bb84

44 files changed

Lines changed: 2733 additions & 1254 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agents/project/llm-models.md

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,46 @@ after any prompt or provider change and put both scores in the PR.
88

99
## Backends
1010

11-
- Ollama (user's own server): the recommended remote path. WebLLM runs the
12-
on-device path in the browser and on Android; it is gated off on iOS
13-
(WKWebView's ~2GB jetsam limit kills model load), where remote Ollama is
14-
the supported path. Apple Foundation Models is the native Apple backend;
15-
a broader native on-device backend is tracked in issue #270.
11+
- Ollama (user's own server) is the most accurate everywhere and the head of
12+
every platform's ranking; `qwen3:8b` is the recommended model. Below it:
13+
iOS runs llama.cpp (Qwen3 4B Instruct) on Metal then Apple Foundation
14+
Models; Android runs Gemini Nano; web runs WebLLM on WebGPU. WebLLM is
15+
gated off on iOS (WKWebView's ~2GB jetsam limit kills model load).
16+
- llama.cpp is iOS-only. It was removed from the Android build (issue #270):
17+
no GPU path there meant ~6.6 tok/s decode against Gemini Nano's ~1.5s
18+
replies, for 76MB of native libraries and a 2.5GB model download. Do not
19+
reintroduce it without a working Vulkan/NPU path and a measured win.
20+
- Gemini Nano (Android system model, ML Kit GenAI Prompt API over AICore) is
21+
the Android system backend. Unlike Apple's it cannot constrain output: ML
22+
Kit structured output is compile-time KSP codegen with no union type, so
23+
no per-tool turn schema and no removable answer branch. It is driven with
24+
the llama.cpp text contract instead. No native tool calling either.
25+
- The two system models, measured 2026-07-28 on the same 90 cases through each
26+
backend's real production path: Gemini Nano (Pixel 10) plans 33/38 and reads
27+
time 48/52; Apple Foundation Models (iPhone 17 Pro Max, iOS 26.5.2) plans
28+
21/38 and reads time 47-51/52. Nano is far better at planning; they are level
29+
on time. The no-tool half is the sharpest split: Nano 8/8, Apple 4/8, with
30+
Apple calling `list_events` three times for "who won the world cup in 2018"
31+
and `get_event` for "delete event 1234".
32+
- Apple's planning deficit is NOT a prompt problem, and the trimmed prompt is
33+
not what causes it. Giving it the whole shared system prompt plus whole tool
34+
descriptions on the planning turn moved 21/38 to 23/38, inside its own
35+
run-to-run spread, and introduced failures that were not there before:
36+
arguments leaking between cases (a German window on two English questions)
37+
and junk in `objectType` ("NO", a tag name). Both attempts were reverted.
38+
What the extra text did fix is narrow and real: `count_events` chosen for a
39+
calendar window went from four failures to one, because that distinction
40+
lives past the first sentence of those two descriptions. Whether the rest is
41+
the model or the 4096-token window is unseparated.
42+
- Apple's output SHAPE is flawless and its judgement is not: every one of its
43+
planning failures was a well-formed, schema-valid tool call that was simply
44+
the wrong call. Constrained decoding fixes shape and buys nothing else.
45+
- AICore rate-limits a burst (`ErrorCode.BUSY`), which is NOT the plugin's own
46+
concurrency guard and must not share its code: running the contract eval
47+
straight after the time eval got every case rejected and reported 0/14 as if
48+
the model had failed them all. `RATE_LIMITED` is its own code now and the
49+
contract eval backs off and retries; a run reports how many retries it took
50+
(11 on the first clean run).
1651

1752
## Measured model floor (prompt-eval, temp 0, 2026-07)
1853

@@ -48,9 +83,50 @@ after any prompt or provider change and put both scores in the PR.
4883
- Apple Foundation Models invents tool arguments (validate before use) and
4984
calls real tools on greeting turns; the first tool call on a tool-less
5085
turn gets a no-tools pushback (578787dc).
86+
- ML Kit GenAI capability docs are wrong on the device; probe at runtime.
87+
`nano-v3` on a Pixel 10 reports `getTokenLimit()` 8192 where the docs say
88+
under 4000, and `isSystemPromptAvailable()` false, so a `SystemInstruction`
89+
is accepted and ignored, silently dropping the tool catalog. Fold the
90+
system text into the prompt when that probe says false.
91+
- AICore refuses inference unless the app is the foreground app
92+
(`BACKGROUND_USE_BLOCKED`) and meters each app daily
93+
(`PER_APP_BATTERY_USE_QUOTA_EXCEEDED`). Both are user-recoverable and need
94+
their own messages; "try again" is wrong advice for either.
95+
- A locked screen kills an on-device eval on BOTH platforms, and silently. On
96+
Android it is `BACKGROUND_USE_BLOCKED` per call; on iOS the app is suspended
97+
and the run simply stops with no report. Set Auto-Lock to Never (iOS) or rely
98+
on `svc power stayon true` (Android) before starting, and treat a run that
99+
produced no report, or far less wall-clock than the case count implies, as
100+
void rather than as a result.
101+
- A dozing or locked screen counts as background, which silently invalidates
102+
any batch run on this backend. A first Gemini Nano eval scored 27/52 with
103+
whole classes at zero; all of those failures were `BACKGROUND_USE_BLOCKED`,
104+
not wrong answers, and the same eval scored 48/52 unlocked. Before trusting
105+
an on-device number, confirm the run has no such error in the log
106+
(`ON_LOCKED` in `dumpsys nfc`, `mWakefulness=Dozing` in `dumpsys power`)
107+
and keep the phone unlocked with the app in front for the whole run. A run
108+
that took far less wall-clock than the case count implies is the tell.
109+
- Never cancel an ML Kit GenAI call. genai-prompt 1.0.0-beta4 is compiled
110+
against kotlinx-coroutines 1.7.3, where `Job.cancel$default` lives in
111+
`Job$DefaultImpls`; the app resolves 1.10.2, which dropped that class, so
112+
ML Kit's cancellation path throws `NoSuchMethodError` on its own thread
113+
pool and kills the process. `GeminiNanoPlugin.cancelChat` abandons the
114+
call instead of cancelling it.
51115

52116
## Eval harness
53117

118+
- Two harnesses, one set of cases. `app/scripts/prompt-eval.mts` scores a
119+
backend over HTTP and so reaches only Ollama. The settings eval row
120+
(`system-model-eval.ts`) scores any `AssistantProvider` on-device, which is
121+
the only way a system model gets a number: neither Apple Foundation Models
122+
nor Gemini Nano has an HTTP surface. It runs both stages, time
123+
(`fm-eval.ts`) and tool contract (`contract-eval.ts`).
124+
- The contract cases live in `contract-eval-cases.ts` and the time cases in
125+
`time-eval-cases.ts`, imported by BOTH harnesses. Never re-declare a case
126+
list in one of them.
127+
- The contract eval passes a recording `runTool`. A backend that owns its tool
128+
loop (Apple) only reveals its calls by executing them, so without it the run
129+
silently scores that backend's fallback path instead of the production one.
54130
- `app/scripts/prompt-eval.mts` imports the production `buildSystemPrompt`;
55131
never fork the prompt text into an eval. A hand-copied prompt drifted
56132
once and measured phantom failures. Two prompt rewrites shipped

app/android/app/build.gradle

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ apply plugin: 'com.android.application'
33
android {
44
namespace "com.zoneminder.zmNinjaNG"
55
compileSdk rootProject.ext.compileSdkVersion
6-
ndkVersion "27.0.12077973"
76

87
signingConfigs {
98
release {
@@ -27,35 +26,12 @@ android {
2726
versionCode 102214
2827
versionName "2.0.0"
2928
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
30-
// Native LLM engine (llama.cpp CPU). arm64 only; no Vulkan/OpenCL (issue #270).
31-
ndk {
32-
abiFilters "arm64-v8a"
33-
}
34-
externalNativeBuild {
35-
cmake {
36-
// FLEXIBLE_PAGE_SIZES: 16KB-page ELF alignment for every .so this
37-
// cmake build links (nativellm + fetched llama/ggml). Required on
38-
// 16KB-page devices (Pixel 8+ era); default only from NDK r28,
39-
// explicit on the pinned r27.
40-
arguments "-DANDROID_STL=c++_shared", "-DCMAKE_BUILD_TYPE=Release", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
41-
}
42-
}
4329
aaptOptions {
4430
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
4531
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
4632
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
4733
}
4834
}
49-
packagingOptions {
50-
jniLibs {
51-
// Extract native libs to disk (pre-AGP-8 behavior). Required: ggml's
52-
// runtime CPU-variant dispatch scans nativeLibraryDir with opendir
53-
// (ggml_backend_load_all_from_path), which cannot see libs packed
54-
// inside the APK - with the AGP 8 default the scan finds zero
55-
// backends and every model load fails instantly (refs #270).
56-
useLegacyPackaging = true
57-
}
58-
}
5935
buildTypes {
6036
release {
6137
signingConfig signingConfigs.release
@@ -71,12 +47,6 @@ android {
7147
sourceCompatibility JavaVersion.VERSION_17
7248
targetCompatibility JavaVersion.VERSION_17
7349
}
74-
externalNativeBuild {
75-
cmake {
76-
path "src/main/cpp/CMakeLists.txt"
77-
version "3.22.1"
78-
}
79-
}
8050
}
8151

8252
repositories {
@@ -99,6 +69,9 @@ dependencies {
9969
implementation "androidx.media3:media3-ui:1.5.1"
10070
implementation "androidx.media3:media3-datasource:1.5.1"
10171
implementation "androidx.media3:media3-session:1.5.1"
72+
// Gemini Nano through AICore (GeminiNanoPlugin, issue #270). Beta API: it carries no
73+
// SLA or deprecation policy, so the version is pinned rather than ranged.
74+
implementation "com.google.mlkit:genai-prompt:1.0.0-beta4"
10275
}
10376

10477
apply from: 'capacitor.build.gradle'

app/android/app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<?xml version="1.0" encoding="utf-8" ?>
2-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
4+
<!-- ML Kit GenAI (GeminiNanoPlugin, issue #270) declares minSdk 26 while this app
5+
supports 24. Overridden rather than raising the app's floor: that would drop
6+
Android 7 users for a backend they could never run anyway, since AICore does not
7+
exist below Android 14. The library contributes no components to the merged
8+
manifest, only a permission and a <queries> entry, and GeminiNanoPlugin refuses
9+
every call below API 26 so none of its classes are touched there. -->
10+
<uses-sdk tools:overrideLibrary="com.google.mlkit.genai.prompt, com.google.mlkit.genai.common" />
311
<application
412
android:allowBackup="true"
513
android:enableOnBackInvokedCallback="false"

app/android/app/src/main/cpp/CMakeLists.txt

Lines changed: 0 additions & 47 deletions
This file was deleted.

0 commit comments

Comments
 (0)