You open the repo on your first morning, the README says "run the app, ask it something," and forty seconds later an answer appears on the phone. Nobody on the team can tell you whether that text came from the bundled on-device model, from a remote endpoint someone configured last quarter, or from a fixture left in a debug build. That uncertainty is not a small onboarding annoyance, because every later measurement you record inherits it.
This post is a first-hour exercise for a junior engineer joining a mobile AI repo. The goal is not to ship a feature, and it is not to improve latency or accuracy; the goal is to make provenance visible, so that when you do open your first PR you already know which execution path you exercised. Everything below is a proposed harness that you run on your own device, not a report of numbers I measured on yours.
Why provenance belongs in hour one
In a normal backend repo you can read the config and know which database answered. In a mobile AI repo the same request can be served by three different things depending on build type, network state, and whether an asset survived the packaging step. If you skip this check, you will eventually write a careful benchmark, show it to your reviewer, and discover the phone was calling a cached response the whole time.
The exercise below gives you three things you can rely on for the rest of onboarding: a build that fails loudly when the model asset is missing, a debug banner that names the source of the last response, and a repeatable way to exercise the remote path on purpose.
The three sources to distinguish
- Bundled asset. A model file inside the app package, loaded from local storage with no network involved.
- Remote endpoint. An HTTP call to a server the app was pointed at, which may be a shared development host or something you started yourself.
- Fixture or cache. A stubbed response in debug builds, or a stored answer replayed from a previous run.
Your banner only needs to name which of these produced the last result. It does not need to be pretty, and it should not ship in release builds.
Step 1: make a missing model asset a build failure
Before you touch any inference code, prove that the file you think is packaged actually is. Add a verification task to the app module and wire it into the check lifecycle. Substitute your own asset name and the checksum your team stores alongside the artifact.
// app/build.gradle.kts (proposed harness)
val modelDir = layout.projectDirectory.dir("src/main/assets/models")
// Placeholder on purpose: copy the real digest from your artifact record.
val expectedDigests = mapOf(
"classifier-small.tflite" to "<sha256-from-your-artifact-record>"
)
tasks.register("verifyModelAssets") {
val dir = modelDir
inputs.dir(dir)
doLast {
expectedDigests.forEach { (name, want) ->
val file = dir.file(name).asFile
require(file.exists()) { "Missing model asset: $name" }
val got = java.security.MessageDigest.getInstance("SHA-256")
.digest(file.readBytes())
.joinToString("") { "%02x".format(it) }
require(got == want) { "Checksum mismatch for $name\nexpected=$want\n got=$got" }
}
println("verifyModelAssets: OK")
}
}
tasks.named("preBuild") { dependsOn("verifyModelAssets") }
Expected observation on a healthy checkout is a single verifyModelAssets: OK line before compilation. Then deliberately rename the asset once, run ./gradlew :app:assembleDebug, and confirm the build stops with the message you wrote. If it compiles anyway, your app has a runtime download path you have not found yet, and that is the most important thing you will learn all day.
Step 2: print the source of the last response
A tiny recorder next to your inference entry point costs almost nothing and removes the guessing. Keep it debug-only, and never log prompt text or user content in it.
// Proposed harness. Field names follow your app's existing types.
sealed interface ModelSource {
data class Bundled(val asset: String, val digestPrefix: String) : ModelSource
data class Remote(val host: String, val path: String) : ModelSource
data class Fixture(val name: String) : ModelSource
}
object Provenance {
private val seen = ArrayDeque<ModelSource>()
fun record(source: ModelSource) {
seen.addLast(source)
if (seen.size > 20) seen.removeFirst()
if (BuildConfig.DEBUG) Log.d("Provenance", describe(source))
}
fun last(): ModelSource? = seen.lastOrNull()
private fun describe(s: ModelSource): String = when (s) {
is ModelSource.Bundled -> "bundled ${s.asset} sha256:${s.digestPrefix}"
is ModelSource.Remote -> "remote ${s.host}${s.path}"
is ModelSource.Fixture -> "fixture ${s.name}"
}
}
Call Provenance.record(...) exactly where the response is produced, not where the request is built, because a retry can change the answer's origin. If you use Compose, a two-line debug overlay reading Provenance.last() is enough; on iOS the same idea maps to a #if DEBUG view or an os_log line.
What the banner buys you
- You can tell a reviewer which path your PR screenshot came from.
- You can catch a silent fallback before it becomes a "works on my machine" thread.
- You can reproduce a bug by forcing the source rather than hoping.
Step 3: exercise the remote path deliberately
Many teams use a hosted endpoint for early integration work, and some projects offer free model access plus a free server option so a new contributor can get a working call without a billing conversation. The open-source project MonkeyCode is one of these, and its operator describes a free model-access tier and a free server option for trying the workflow; the same operator-supplied description puts the free allowance at 10 million tokens. Treat that figure as a starting point, not a contract, and re-read the project's current terms before you plan any budget around it.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The useful part for onboarding is narrower than the marketing: a shared free endpoint lets you prove that your client's request shape, timeout, and error handling work before you depend on a host only three people can reach. Time the first call rather than trusting it.
# Shape only. Use the base URL and path from the project's own setup docs.
BASE_URL="https://<your-endpoint-from-the-project-docs>"
for i in 1 2 3; do
curl -sS -o /dev/null \
-w "attempt=%{http_code} ttfb=%{time_starttransfer}s total=%{time_total}s\n" \
-H "Content-Type: application/json" \
-d '{"prompt":"ping","max_tokens":8}' \
"$BASE_URL/v1/complete"
done
Record three numbers per attempt and note whether the first one is slower than the rest, which would suggest a cold path rather than a steady one. Do not run a load test against shared free infrastructure; three sequential calls teach you what a first-hour contributor needs, and concurrency experiments belong on a host your team controls.
When the banner disagrees with your mental model
SymptomLikely broken pathFirst checkBuild passes with the asset deletedSilent download or remote fallbackGrep for the model URL in build configBanner says
remote in airplane modeCached response or fixtureClear app storage, rerunBanner says
bundled but answers varyPrompt template changedDiff template file against
mainFirst remote call takes much longerCold endpoint or cold radioRepeat three times, note radio stateWorks after reinstall onlyAsset copied at first launchInspect first-run copy logic
Each row is a rollback condition, not a curiosity. If your banner reports remote while the device has no network, you have found something worth raising before you write feature code.
Evidence to record before your first PR
Fill this in on your own hardware and paste it into the PR description or your onboarding notes. Reviewers trust a filled table far more than a sentence claiming it works.
- Device model and OS version, plus whether it is a physical device or a simulator.
- Framework and dependency versions, including the inference runtime and build tool versions.
- Build type used for the observation, and the exact command you ran.
- Network state during each step, including radio type and whether airplane mode was toggled.
- Permission state for any sensor the feature reads, and whether you revoked it mid-run.
- Power conditions, such as plugged in versus battery, and any low-power mode.
- Which provenance value the banner printed for each observation.
Limitations and who should skip this
This harness assumes your app can load a model from local storage. If your product is server-only inference, a bundled-asset check adds noise and you should stop after step 2 and document the endpoint instead. Teams with a managed model registry may already enforce digests in CI, in which case duplicating the check locally is waste. Checksumming a multi-hundred-megabyte asset on every build slows feedback loops noticeably, so scope the task to debug variants or run it on a schedule.
Finally, the banner is diagnostic tooling. It must be stripped or gated in release builds, because hostnames and asset digests are information you gain nothing from publishing.
If you run this in your own repo, I would like to see the environment rather than the conclusion: device, OS, framework versions, the transition steps you used, and whether the banner recovered, restarted, or silently disappeared. If you want a place to start the remote path without provisioning a host first, the project's free tier and free server option are the shortest route to a first honest measurement.#ai #mobile #android #testing #software #coding #development #engineering #inclusive #community
Know Which Model Answered: A First-Hour Provenance Banner for Mobile AI Repos