Android requires versionCode to increase for update-over-top installs.
Without this, reinstalling would say "App not installed" on some devices.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chat:
- ChatActivity with RecyclerView message list (user/AI bubbles)
- Streams tokens directly from LiteRT model via generateStreaming()
- Maintains full conversation history (multi-turn context)
- Respects auto_system_prompt setting from SharedPreferences
- "Chat" button in MainActivity, enabled only when server is running
- "⚡ Generating…" indicator while model is thinking
- Log on main screen unchanged — still shows all requests
Server:
- Settings card: temperature slider, max tokens, system prompt toggle
- "⚡ Processing request…" indicator in status card during inference
- onActiveRequest callback wired through service → activity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes:
- Streaming was bypassed for all requests (system prompt auto-inject made
messages.size always > 1). Stream=true now routes directly to generateStreaming
before multi-turn check
- LiteRTModel: add Mutex to serialize Engine calls (not thread-safe)
New features:
- Settings card: temperature slider, max tokens, agent system prompt toggle
(all saved to SharedPreferences, applied on server start)
- Active request indicator: "⚡ Processing request…" shown in status card
while inference is running
- onActiveRequest callback from AIApiServer → ApiServerService → MainActivity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Google moved all models to HuggingFace (old CDN returns 404).
All downloads now require a HuggingFace API token.
- Add HF token input field (saved to SharedPreferences, restored on relaunch)
- Download uses Bearer auth header
- Switch to Gemma 3 1B IT (litert-community) — .task format, works with
current MediaPipe API, 555 MB Q4 or 1 GB Q8
- Clear auth error messages (401/403 shown to user)
- Gemma 3n E4B/E2B (.litertlm format) requires runtime upgrade — planned next
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ModelSpec enum with 3 downloadable options from MediaPipe CDN
- Gemma 3n E4B set as recommended default (best coding/reasoning via MoE)
- Gemma 3n E2B as faster/smaller alternative
- Gemma 2B kept as lightest option
- UI shows all 3 download buttons, hides all once any model is installed
- Installed model name shown in status (e.g. "✓ Gemma 3n E4B ready")
- Custom models (DeepSeek Coder, Qwen2.5-Coder) can be manually placed in files dir
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: ML Kit Gemini Nano enforces ErrorCode 30 (foreground restriction)
at the AICore binder level. No foreground service or wake lock can bypass it —
the app's Activity must be the visible top window.
Fix: MediaPipe LLM Inference runs entirely in the app process via the Tensor G5
GPU (OpenCL/Vulkan), with no AICore dependency and no foreground restriction.
Changes:
- OnDeviceModel.create() now tries MediaPipe FIRST, Nano second
- ModelDownloader.kt: downloads Gemma 2B IT GPU INT4 (~1.3GB) from Google's
MediaPipe model CDN with resume support (Range header)
- MainActivity: "Download Model" card shows download status and progress bar;
auto-hides once model is present; uses lifecycleScope for coroutine
- Layout: model card inserted between status and port field
- Strings: btn_download_model, model_downloaded, model_not_downloaded
Once the model is downloaded the server accepts requests in the background
indefinitely with no foreground Activity required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AgentConfig.kt defines the full agent configuration optimised for Gemini
Nano on the Tensor G5 chip:
System prompt (~700 tokens):
- Five explicit workflow stages: EXPLORE → PLAN → CHANGE → VERIFY → DONE
- Hard rules: one tool per turn, 1-3 sentence replies, 120-line read limit,
max 3 files per task, patch_file preferred over write_file
Seven tools (OpenAI function-calling format):
read_file(path, start_line?, end_line?) — sectioned reads, max 120 lines
write_file(path, content) — new files / full rewrites < 80 ln
patch_file(path, old_str, new_str) — targeted in-place edits (preferred)
list_dir(path, depth?) — directory structure
search_code(pattern, path?, include?) — regex search across files
run_command(command, cwd?) — build, test, lint
task_done(summary, files_changed?) — explicit completion signal
AIApiServer changes:
- Auto-injects the agent system prompt when the conversation has no system
message, and auto-injects DEFAULT_TOOLS when the request provides none.
Makes the server zero-config for any OpenAI-compatible agent client.
- New GET /v1/agent endpoint returns system_prompt + tools + notes as JSON
so clients like OpenClaw can fetch the config and apply it automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Delete GeminiCloudModel.kt entirely. All inference now runs through
Gemini Nano on the Tensor G5 TPU via ML Kit, with MediaPipe as fallback
for custom local model files. No data leaves the device.
- OnDeviceModel.create() reverts to Nano → MediaPipe chain, no apiKey param
- Removed: API key UI, SharedPreferences key storage, cloud model routing
- Removed: thinking mode (cloud-only feature)
- Removed: pixel10-fast / pixel10-thinking model IDs → single "pixel10" model
- /v1/models now reports honest on-device limits (4096 ctx, 1024 output)
- CI smoke test updated: installs APK on emulator and verifies package,
no inference tests (require real Pixel hardware with Tensor chip)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- max_tokens default: 1024 → 8192 (enough for full functions and files)
- thinking mode max_tokens default: 2048 → 16384
- chat() default: 1024 → 8192
- ModelInfo gains context_length (1_000_000) and max_output_tokens fields
so OpenClaw and other agent frameworks can auto-configure correctly
- /v1/models now advertises full 1M input context for both models
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OnDeviceModel gains a chat() method accepting the full message history and
a list of ToolDef entries. Returns ChatResult which is either a text reply or
a list of ToolCallData the model wants to invoke. Default impl flattens
messages to a prompt so Nano and MediaPipe backends work unchanged.
GeminiCloudModel overrides chat() with native Gemini function calling:
- Converts OpenAI tools to Gemini functionDeclarations
- Handles system messages via Gemini's systemInstruction field
- Converts multi-turn history including assistant tool_calls and tool results
(role=tool → Gemini functionResponse with name resolved from prior turns)
- Parses functionCall parts in the response and returns ToolCallData list
- Falls back to text content when no function call is present
AIApiServer routes to chat() when tools are provided or the conversation
has more than one turn. Returns finish_reason=tool_calls and the tool_calls
array in the assistant message so OpenClaw / any OpenAI-compatible agent
client can execute tools and feed results back.
ApiModels updated: Message.content nullable, tool_calls and tool_call_id
added to Message, Tool/ToolFunction/ToolCall/FunctionCallDetail added,
tools and tool_choice added to ChatRequest.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expose two model IDs matching the Gemini app's modes:
- pixel10-fast: Gemini 2.0 Flash, low latency, no reasoning trace
- pixel10-thinking: Gemini 2.5 Flash, step-by-step reasoning before answering
OnDeviceModel gains generateWithThinking() returning ThinkingResult
(thinking: String, response: String). Default impl delegates to generate()
so Nano and MediaPipe backends work unchanged.
GeminiCloudModel overrides generateWithThinking() to call
gemini-2.5-flash-preview-04-17 with thinkingConfig.thinkingBudget. Parts
with thought=true are collected as the reasoning trace; remaining parts form
the final answer.
ChatRequest gains thinking_budget (0 = fast, >0 = thinking) and model fields.
AIApiServer routes to thinking mode when model name contains "think" or
thinking_budget > 0. Thinking responses include a non-standard thinking field
in Choice alongside the normal content. /v1/models lists both model IDs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add GeminiCloudModel: proxies inference to Gemini 2.0 Flash via HTTPS
using HttpURLConnection (no new deps). Supports both blocking and SSE
streaming. Works from any context — background, emulator, CI.
- Fix background inference: OnDeviceModel.create() now accepts an apiKey;
when set, GeminiCloudModel is selected immediately, bypassing Gemini
Nano's foreground-only restriction. ApiServerService reads the key from
SharedPreferences at startup.
- Add API key UI: password field in MainActivity saved to SharedPreferences
before the service starts; disabled while server is running.
- Add GitHub Actions CI (.github/workflows/ci.yml): build job produces a
debug APK artifact; test job spins up a KVM-accelerated Android 31
emulator, installs the APK, writes the GEMINI_API_KEY secret into
SharedPreferences via adb run-as, starts the service, and runs curl
assertions against /health, /v1/models, /v1/chat/completions, and the
SSE streaming endpoint.
- Add release workflow (.github/workflows/release.yml): triggered on v*
tags, builds the APK and creates a GitHub Release with it attached.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add adaptive app icon with AI chip + signal wave vector design
- Add Apache 2.0 LICENSE file
- Rewrite README with badges, full API docs, Python examples,
device compatibility table, dependency licenses, and ToS disclaimer
- Extract all hardcoded layout strings to strings.xml
- Add roundIcon support in AndroidManifest
- Add GitHub community files: issue templates, PR template,
CONTRIBUTING.md, SECURITY.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bump Kotlin from 2.0.21 to 2.1.20 for ML Kit genai metadata 2.2.0 compat
- Fix GeminiNanoModel imports: DownloadStatus/FeatureStatus moved to genai.common,
TextPart/generateContentRequest moved out of .type subpackage
- Fix MediaPipeModel: replace removed setTopK/setTemperature/setRandomSeed
with setMaxTopK (MediaPipe 0.10.24 API)
- Fix gradlew: remove broken lines that passed GradleWrapperMain as task arg
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- GeminiNanoModel: Rewrite to use actual ML Kit Prompt API
(Generation.getClient(), checkStatus(), download(), warmup())
- MediaPipeModel: Fix streaming to use sync fallback since
MediaPipe requires result listener set at build time
- ApiModels: Add @SerializedName("object") for OpenAI compat,
add "created" timestamp to ChatResponse/StreamChunk
- settings.gradle.kts: Fix dependencyResolutionManagement typo
- MainActivity: Use GradientDrawable.setColor() to preserve
oval shape on status dot
- Add Gradle wrapper scripts (gradlew, gradlew.bat, jar)
https://claude.ai/code/session_01GvqMLSMmfMR8uz66BFVXX2
Android app that turns a Pixel 10 into a free AI API server by leveraging
the Tensor G5's on-device AI capabilities through an embedded HTTP server.
Key components:
- Dual AI backend: Gemini Nano (ML Kit Prompt API) + MediaPipe LLM fallback
- OpenAI-compatible REST API (chat/completions, completions, models, health)
- NanoHTTPD embedded server with CORS support and SSE streaming
- Foreground service with wake lock for persistent background operation
- Material Design 3 dashboard with live request logging
All inference runs entirely on-device — zero cloud costs, full offline capability.
https://claude.ai/code/session_01GvqMLSMmfMR8uz66BFVXX2