cd /news/ai-tools/know-which-model-answered-a-first-ho… · home › topics › ai-tools › article
[ARTICLE · art-158718] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=· neutral

Know Which Model Answered: A First-Hour Provenance Banner for Mobile AI Repos

A developer outlined a first-hour onboarding harness for mobile AI repositories that makes model provenance visible: a Gradle verifyModelAssets task that fails the build when a bundled model asset is missing or its SHA-256 digest mismatches, a debug-only recorder that tags each inference response as Bundled, Remote, or Fixture, and a repeatable way to exercise the remote path deliberately. The proposed harness is meant to be run on the reader's own device rather than reporting measured results, and the banner is intended to stay out of release builds.

by read6 min views1 publishedOct 27, 2026

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.

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.

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.

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.

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.

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.

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.

Symptom Likely broken path First check
Build passes with the asset deleted Silent download or remote fallback Grep for the model URL in build config
Banner says remote in airplane mode Cached response or fixture Clear app storage, rerun
Banner says bundled but answers vary Prompt template changed Diff template file against main
First remote call takes much longer Cold endpoint or cold radio Repeat three times, note radio state
Works after reinstall only Asset copied at first launch Inspect 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.

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.

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.

── more in #ai-tools 4 stories · sorted by recency
── more on @gradle 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/know-which-model-ans…] indexed:0 read:6min 2026-10-27 · —