ANTIGRAVITY LABJP
Articles/Integrations
Integrations/2026-04-11Advanced

Gemma 4 On-Device AI Integration × Antigravity Custom Models — Advanced Workflow Guide

A comprehensive guide covering Gemma 4 on-device AI integration, fine-tuning, custom model deployment with Antigravity, and building production-ready advanced workflows.

gemma-419gamma-42on-device-ai5fine-tuning7custom-modelantigravity429edge-ai3litert2production71

With the release of Gemma 4, on-device AI development has entered a new era. The E2B model runs on roughly 2GB of memory, and the E4B delivers function-calling capabilities with under 4B effective parameters. This isn't just demo material — it means production-quality AI features can now be embedded directly into edge devices.

This article takes a deep dive into Gemma 4's on-device AI integration, walking you through advanced workflows for building, optimizing, and deploying custom models within the Antigravity development environment, complete with implementation code.

Architecture Design for On-Device AI

Building production-quality on-device AI requires more than just picking a model — you need to design the entire inference pipeline. Let's explore practical architecture patterns built around Gemma 4's four model sizes.

Hybrid Architecture: Optimal Edge + Cloud Distribution

Not every AI task needs to run on-device. The most efficient approach distributes work between edge and cloud based on task complexity.

# Hybrid inference router implementation
class HybridInferenceRouter:
    """Automatically routes tasks between edge and cloud based on complexity"""
    
    def __init__(self, edge_model, cloud_client):
        self.edge_model = edge_model      # Gemma 4 E4B (local)
        self.cloud_client = cloud_client  # Gemma 4 31B (server)
        self.complexity_threshold = 0.7
    
    async def route(self, task: str, context: dict) -> str:
        complexity = self._estimate_complexity(task, context)
        
        if complexity < self.complexity_threshold:
            # Simple tasks -> process instantly on edge
            return await self.edge_model.generate(task)
        else:
            # Complex tasks -> delegate to cloud
            if self._is_network_available():
                return await self.cloud_client.generate(task)
            else:
                # Offline fallback: best-effort on edge
                return await self.edge_model.generate(
                    task, 
                    max_tokens=2048,
                    temperature=0.3  # Prioritize accuracy
                )
    
    def _estimate_complexity(self, task: str, context: dict) -> float:
        """Estimate task complexity on a 0.0-1.0 scale"""
        score = 0.0
        if len(task.split()) > 200:
            score += 0.3
        if context.get("code_lines", 0) > 500:
            score += 0.3
        if any(kw in task.lower() for kw in ["analyze", "compare", "design", "refactor"]):
            score += 0.2
        return min(score, 1.0)
 
    def _is_network_available(self) -> bool:
        """Check network connectivity"""
        import socket
        try:
            socket.create_connection(("8.8.8.8", 53), timeout=1)
            return True
        except OSError:
            return False
 
# Usage:
# router = HybridInferenceRouter(edge_model, cloud_client)
# result = await router.route("Review this code", {"code_lines": 50})

Memory Management and Model Lifecycle

Memory is the primary constraint on edge devices. Efficient load/unload lifecycle management for Gemma 4 models is essential.

// iOS: Core ML and Gemma 4 memory management
import CoreML
 
class GemmaModelManager {
    private var loadedModel: MLModel?
    private let modelURL: URL
    private let memoryThresholdMB: Int = 150
    
    init(modelPath: String) {
        self.modelURL = Bundle.main.url(
            forResource: modelPath, 
            withExtension: "mlmodelc"
        )!
    }
    
    /// Load model based on available memory
    func loadIfNeeded() throws -> MLModel {
        if let model = loadedModel {
            return model
        }
        
        let availableMB = getAvailableMemoryMB()
        guard availableMB > memoryThresholdMB else {
            throw GemmaError.insufficientMemory(
                available: availableMB,
                required: memoryThresholdMB
            )
        }
        
        let config = MLModelConfiguration()
        config.computeUnits = .cpuAndNeuralEngine  // Leverage ANE
        
        let model = try MLModel(contentsOf: modelURL, configuration: config)
        self.loadedModel = model
        
        // Auto-unload on memory warning
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleMemoryWarning),
            name: UIApplication.didReceiveMemoryWarningNotification,
            object: nil
        )
        
        return model
    }
    
    @objc private func handleMemoryWarning() {
        loadedModel = nil  // Release the model
        print("[Gemma] Memory warning — model unloaded")
    }
    
    private func getAvailableMemoryMB() -> Int {
        var info = mach_task_basic_info()
        var count = mach_msg_type_number_t(
            MemoryLayout<mach_task_basic_info>.size
        ) / 4
        let result = withUnsafeMutablePointer(to: &info) {
            $0.withMemoryRebound(
                to: integer_t.self, capacity: 1
            ) {
                task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
            }
        }
        guard result == KERN_SUCCESS else { return 0 }
        let usedMB = Int(info.resident_size) / 1_048_576
        let totalMB = Int(ProcessInfo.processInfo.physicalMemory) / 1_048_576
        return totalMB - usedMB
    }
}
 
enum GemmaError: Error {
    case insufficientMemory(available: Int, required: Int)
}

Building Custom Models with Fine-Tuning

Gemma 4's Apache 2.0 license means you can fine-tune freely. Here's how to build domain-specific custom models.

Efficient Fine-Tuning with LoRA

Full-parameter fine-tuning is computationally expensive. LoRA (Low-Rank Adaptation) makes it possible to fine-tune the 26B MoE model on just 16GB of GPU memory.

# Gemma 4 LoRA fine-tuning implementation
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
import torch
 
# 4-bit quantization to reduce memory usage
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True
)
 
# Load model and tokenizer
model_id = "google/gemma-4-27b-it"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto",
    torch_dtype=torch.bfloat16
)
 
# LoRA configuration
lora_config = LoraConfig(
    r=16,                        # LoRA rank
    lora_alpha=32,               # Scaling factor
    target_modules=[
        "q_proj", "k_proj", "v_proj",  # Attention layers
        "o_proj", "gate_proj",
        "up_proj", "down_proj"
    ],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)
 
# Apply LoRA
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, lora_config)
 
# Verify trainable parameters
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())
print(f"Trainable: {trainable_params:,} / Total: {total_params:,}")
print(f"Trainable ratio: {100 * trainable_params / total_params:.2f}%")
# Output: Trainable: 41,943,040 / Total: 27,227,128,832
# Output: Trainable ratio: 0.15%

Training Data Preparation and Quality Control

The success of fine-tuning depends on data quality. Here's an example of building a domain-specific Q&A dataset.

# Structuring and validating training data
from datasets import Dataset
import json
 
def prepare_training_data(raw_data_path: str) -> Dataset:
    """Load raw data and convert to Gemma 4 format"""
    
    with open(raw_data_path, "r", encoding="utf-8") as f:
        raw_data = json.load(f)
    
    formatted = []
    skipped = 0
    
    for item in raw_data:
        # Quality checks
        if len(item["question"]) < 10:
            skipped += 1
            continue
        if len(item["answer"]) < 50:
            skipped += 1
            continue
        
        # Convert to Gemma 4 chat template format
        text = (
            f"<start_of_turn>user\n{item['question']}<end_of_turn>\n"
            f"<start_of_turn>model\n{item['answer']}<end_of_turn>"
        )
        formatted.append({"text": text})
    
    print(f"Valid samples: {len(formatted)} / Skipped: {skipped}")
    return Dataset.from_list(formatted)
 
# Usage:
# dataset = prepare_training_data("training_data.json")
# Output: Valid samples: 4,523 / Skipped: 47

Running Training and Evaluating the Model

# Training execution with Transformers Trainer
from transformers import TrainingArguments, Trainer
 
training_args = TrainingArguments(
    output_dir="./gemma4-custom",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    weight_decay=0.01,
    warmup_ratio=0.03,
    lr_scheduler_type="cosine",
    logging_steps=10,
    save_strategy="epoch",
    bf16=True,
    optim="paged_adamw_8bit",
    gradient_checkpointing=True,
    max_grad_norm=0.3,
)
 
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    tokenizer=tokenizer,
)
 
# Start training
trainer.train()
 
# Save the LoRA adapter
model.save_pretrained("./gemma4-custom-lora")
print("Custom model saved successfully")
# Output: Custom model saved successfully

Integrating Custom Models with Antigravity Agents

Here's how to wire your fine-tuned Gemma 4 model into Antigravity's agent system.

Building a Custom Model Server

First, convert your custom model into a format Ollama can host.

# Converting custom model to Ollama format
modelfile_content = """
FROM ./gemma4-custom-merged.gguf
 
PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER num_ctx 8192
 
SYSTEM You are an AI assistant with domain-specific expertise.
Provide accurate and practical answers.
"""
 
with open("Modelfile", "w") as f:
    f.write(modelfile_content)
 
# Run in shell:
# ollama create gemma4-custom -f Modelfile
# ollama run gemma4-custom "Test question"

Defining Custom Models in Antigravity agents.md

<!-- .antigravity/agents.md -->
# Domain Expert Agent
 
## Overview
Uses a custom fine-tuned Gemma 4 model to provide
domain-specific assistance for the project.
 
## Model Configuration
- Base: Gemma 4 26B MoE
- Adapter: LoRA (domain-specific)
- Endpoint: http://localhost:11434/api/generate
- Model name: gemma4-custom
 
## Tasks
1. Code suggestions based on project-specific design patterns
2. Documentation generation with domain terminology understanding
3. Bug detection based on historical implementation patterns
 
## Context
- Can reference /docs/** documentation
- Can analyze /src/** source code

Integration as an MCP (Model Context Protocol) Server

Exposing your custom Gemma 4 model as an MCP server makes it directly accessible from Antigravity.

// MCP Server: Custom Gemma 4 Model
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 
const server = new Server(
  { name: "gemma4-custom", version: "1.0.0" },
  { capabilities: { tools: {} } }
);
 
// Domain-specific inference tool
server.setRequestHandler("tools/list", async () => ({
  tools: [{
    name: "domain_query",
    description: "Answer domain-specific questions using custom Gemma 4",
    inputSchema: {
      type: "object",
      properties: {
        question: { type: "string", description: "The question to answer" },
        context: { type: "string", description: "Additional context" }
      },
      required: ["question"]
    }
  }]
}));
 
server.setRequestHandler("tools/call", async (request) => {
  if (request.params.name === "domain_query") {
    const { question, context } = request.params.arguments as {
      question: string;
      context?: string;
    };
    
    // Query the custom model via Ollama
    const response = await fetch("http://localhost:11434/api/generate", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        model: "gemma4-custom",
        prompt: context 
          ? `Context:\n${context}\n\nQuestion: ${question}` 
          : question,
        stream: false
      })
    });
    
    const data = await response.json();
    return { content: [{ type: "text", text: data.response }] };
  }
  throw new Error(`Unknown tool: ${request.params.name}`);
});
 
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);
// Run: npx tsx mcp-gemma4-server.ts

Optimizing Edge Deployment

Key optimization techniques for deploying custom models to edge devices.

Quantization and Model Compression

# GGUF conversion and quantization pipeline
# Using llama.cpp for quantization
 
# Step 1: HuggingFace format -> GGUF conversion
# python convert_hf_to_gguf.py ./gemma4-custom-merged \
#   --outfile gemma4-custom-f16.gguf --outtype f16
 
# Step 2: Quantize (Q4_K_M recommended — best quality/size balance)
# ./quantize gemma4-custom-f16.gguf gemma4-custom-q4km.gguf Q4_K_M
 
# Quantization comparison:
# Q8_0:  ~27GB -> High quality but too large for edge
# Q4_K_M: ~15GB -> Best balance of quality and size (recommended)
# Q4_0:  ~14GB -> Smallest size but some quality loss
 
# Python quality verification for quantized models
def evaluate_quantized_model(
    original_model_path: str, 
    quantized_model_path: str,
    test_prompts: list[str]
) -> dict:
    """Compare output quality between original and quantized models"""
    from rouge_score import rouge_scorer
    
    scorer = rouge_scorer.RougeScorer(
        ["rouge1", "rouge2", "rougeL"], 
        use_stemmer=True
    )
    
    results = {"rouge1": [], "rouge2": [], "rougeL": []}
    
    for prompt in test_prompts:
        original_output = generate(original_model_path, prompt)
        quantized_output = generate(quantized_model_path, prompt)
        
        scores = scorer.score(original_output, quantized_output)
        for key in results:
            results[key].append(scores[key].fmeasure)
    
    avg_results = {
        k: sum(v) / len(v) for k, v in results.items()
    }
    print(f"Quantization quality scores: {avg_results}")
    return avg_results
    # Output: Quantization quality scores: {'rouge1': 0.92, 'rouge2': 0.87, 'rougeL': 0.90}

Android LiteRT-LM Deployment Pipeline

// Android: Production integration for custom Gemma 4 model
class GemmaInferenceManager(
    private val context: Context
) {
    private var session: LlmInferenceSession? = null
    
    companion object {
        private const val MODEL_FILE = "gemma4-e4b-custom.task"
        private const val MAX_TOKENS = 1024
        private const val TEMPERATURE = 0.7f
    }
    
    /**
     * Initialize model (run on background thread)
     */
    suspend fun initialize(): Result<Unit> = withContext(Dispatchers.IO) {
        runCatching {
            val options = LlmInference.LlmInferenceOptions.builder()
                .setModelPath(getModelPath())
                .setMaxTokens(MAX_TOKENS)
                .setTemperature(TEMPERATURE)
                .setTopK(40)
                .setRandomSeed(42)
                .build()
            
            val inference = LlmInference.createFromOptions(context, options)
            session = inference.createSession()
        }
    }
    
    /**
     * Streaming inference
     */
    fun generateStream(
        prompt: String,
        onToken: (String) -> Unit,
        onComplete: () -> Unit,
        onError: (Exception) -> Unit
    ) {
        val activeSession = session ?: run {
            onError(IllegalStateException("Model not initialized"))
            return
        }
        
        try {
            activeSession.addQueryChunk(prompt)
            activeSession.generateResponseAsync(
                object : LlmInferenceSession.ResponseCallback {
                    override fun onPartialResponse(text: String) {
                        onToken(text)
                    }
                    override fun onResponse(text: String) {
                        onComplete()
                    }
                    override fun onError(e: Exception) {
                        onError(e)
                    }
                }
            )
        } catch (e: Exception) {
            onError(e)
        }
    }
    
    private fun getModelPath(): String {
        return File(context.filesDir, MODEL_FILE).absolutePath
    }
    
    fun release() {
        session?.close()
        session = null
    }
}
 
// Jetpack Compose usage:
// val manager = remember { GemmaInferenceManager(context) }
// LaunchedEffect(Unit) { manager.initialize() }

Production Workflow

Building a stable production workflow for custom model operations.

A/B Testing Framework

# A/B testing between model versions
import json
from datetime import datetime
from typing import Optional
 
class ModelABTest:
    """A/B testing framework for custom Gemma 4 models"""
    
    def __init__(
        self,
        model_a: str,  # Control (current model)
        model_b: str,  # Variant (new model)
        traffic_ratio: float = 0.5,
        log_path: str = "ab_test_logs.jsonl"
    ):
        self.model_a = model_a
        self.model_b = model_b
        self.traffic_ratio = traffic_ratio
        self.log_path = log_path
    
    def select_model(self, user_id: str) -> str:
        """Consistent model assignment based on user ID"""
        hash_val = hash(user_id) % 100
        selected = self.model_b if hash_val < (self.traffic_ratio * 100) else self.model_a
        return selected
    
    async def generate_and_log(
        self, 
        user_id: str, 
        prompt: str,
        feedback: Optional[str] = None
    ) -> dict:
        """Run inference + log results"""
        model = self.select_model(user_id)
        
        start = datetime.now()
        response = await self._call_model(model, prompt)
        latency_ms = (datetime.now() - start).total_seconds() * 1000
        
        log_entry = {
            "timestamp": datetime.now().isoformat(),
            "user_id": user_id,
            "model": model,
            "prompt_length": len(prompt),
            "response_length": len(response),
            "latency_ms": round(latency_ms, 2),
            "feedback": feedback
        }
        
        with open(self.log_path, "a") as f:
            f.write(json.dumps(log_entry) + "\n")
        
        return {"response": response, "model": model, "latency_ms": latency_ms}
    
    async def _call_model(self, model: str, prompt: str) -> str:
        """Call model via Ollama"""
        import httpx
        async with httpx.AsyncClient(timeout=30.0) as client:
            resp = await client.post(
                "http://localhost:11434/api/generate",
                json={"model": model, "prompt": prompt, "stream": False}
            )
        return resp.json()["response"]
 
# Usage:
# ab_test = ModelABTest("gemma4-custom-v1", "gemma4-custom-v2", traffic_ratio=0.2)
# result = await ab_test.generate_and_log("user_123", "Query text")

Monitoring and Alerts

# Production monitoring for custom models
from dataclasses import dataclass, field
from collections import deque
import statistics
 
@dataclass
class ModelMetrics:
    """Inference metrics collection and anomaly detection"""
    
    window_size: int = 100
    latencies: deque = field(default_factory=lambda: deque(maxlen=100))
    error_count: int = 0
    total_requests: int = 0
    
    # Alert thresholds
    latency_p95_threshold_ms: float = 3000.0
    error_rate_threshold: float = 0.05
    
    def record_request(self, latency_ms: float, success: bool):
        self.total_requests += 1
        self.latencies.append(latency_ms)
        if not success:
            self.error_count += 1
    
    def check_health(self) -> dict:
        if len(self.latencies) < 10:
            return {"status": "warming_up", "alerts": []}
        
        alerts = []
        sorted_latencies = sorted(self.latencies)
        p95_idx = int(len(sorted_latencies) * 0.95)
        p95 = sorted_latencies[p95_idx]
        
        if p95 > self.latency_p95_threshold_ms:
            alerts.append(
                f"P95 latency exceeded: {p95:.0f}ms "
                f"(threshold: {self.latency_p95_threshold_ms:.0f}ms)"
            )
        
        error_rate = self.error_count / self.total_requests
        if error_rate > self.error_rate_threshold:
            alerts.append(
                f"Error rate exceeded: {error_rate:.2%} "
                f"(threshold: {self.error_rate_threshold:.2%})"
            )
        
        return {
            "status": "healthy" if not alerts else "degraded",
            "p50_ms": statistics.median(self.latencies),
            "p95_ms": p95,
            "error_rate": error_rate,
            "total_requests": self.total_requests,
            "alerts": alerts
        }
 
# Usage:
# metrics = ModelMetrics()
# metrics.record_request(latency_ms=250.0, success=True)
# health = metrics.check_health()
# Output: {"status": "healthy", "p50_ms": 245.0, "p95_ms": 890.0, ...}

Advanced Workflow: CI/CD Pipeline Integration

Automating the entire pipeline from model training to deployment.

# .github/workflows/gemma4-model-pipeline.yml
name: Gemma 4 Custom Model Pipeline
 
on:
  push:
    paths:
      - 'training_data/**'
      - 'model_config/**'
 
jobs:
  train-and-evaluate:
    runs-on: [self-hosted, gpu]
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      
      - name: Install dependencies
        run: pip install -r requirements-training.txt
      
      - name: Train LoRA adapter
        run: python scripts/train_lora.py --config model_config/config.yaml
        env:
          HF_TOKEN: ${{ secrets.HF_TOKEN }}
      
      - name: Evaluate model quality
        run: |
          python scripts/evaluate.py \
            --model ./output/gemma4-custom-lora \
            --benchmark ./eval_data/benchmark.json \
            --threshold 0.85
      
      - name: Convert to GGUF
        if: success()
        run: |
          python scripts/merge_and_convert.py \
            --base google/gemma-4-27b-it \
            --lora ./output/gemma4-custom-lora \
            --output ./output/gemma4-custom.gguf \
            --quantize Q4_K_M
      
      - name: Upload model artifact
        if: success()
        uses: actions/upload-artifact@v4
        with:
          name: gemma4-custom-model
          path: ./output/gemma4-custom-q4km.gguf
  
  deploy:
    needs: train-and-evaluate
    runs-on: ubuntu-latest
    steps:
      - name: Download model artifact
        uses: actions/download-artifact@v4
        with:
          name: gemma4-custom-model
      
      - name: Deploy to model server
        run: |
          scp gemma4-custom-q4km.gguf ${{ secrets.MODEL_SERVER }}:/models/
          ssh ${{ secrets.MODEL_SERVER }} "ollama create gemma4-custom -f /models/Modelfile"
          echo "Deployment complete"

Production Operations Patterns — Caching, Batching, and Parameter Tuning

When running fine-tuned custom models in production, balancing cost-efficiency against response time requires inference-layer engineering: caching identical queries, batching multiple queries, and tuning inference parameters. These are the practical patterns.

Inference Caching

In production, identical prompts arrive repeatedly. Caching reduces second-call latency from 2+ seconds to under 100ms.

import hashlib
import time
from typing import Optional
 
class GemmaInferenceCache:
    def __init__(self, max_cache_size: int = 1000):
        self.cache = {}
        self.access_times = {}
        self.max_size = max_cache_size
    
    def _hash_prompt(self, prompt: str, model: str) -> str:
        key_str = f"{model}:{prompt}"
        return hashlib.sha256(key_str.encode()).hexdigest()
    
    def get(self, prompt: str, model: str = "gemma4-instruct") -> Optional[str]:
        key = self._hash_prompt(prompt, model)
        if key in self.cache:
            self.access_times[key] = time.time()
            return self.cache[key]
        return None
    
    def set(self, prompt: str, result: str, model: str = "gemma4-instruct"):
        key = self._hash_prompt(prompt, model)
        if len(self.cache) >= self.max_size:
            oldest_key = min(self.access_times, key=self.access_times.get)
            del self.cache[oldest_key]
            del self.access_times[oldest_key]
        self.cache[key] = result
        self.access_times[key] = time.time()

Batch Processing

Process multiple prompts in parallel using ThreadPoolExecutor.

from concurrent.futures import ThreadPoolExecutor
from typing import List, Tuple
 
class GemmaBatchProcessor:
    def __init__(self, max_workers: int = 4):
        self.executor = ThreadPoolExecutor(max_workers=max_workers)
        self.model = "gemma4-instruct"
    
    def _process_single(self, prompt: str) -> Tuple[str, str]:
        try:
            import ollama
            response = ollama.generate(
                model=self.model,
                prompt=prompt,
                stream=False,
                options={"num_predict": 256}
            )
            return (prompt, response["response"])
        except Exception as e:
            return (prompt, f"ERROR: {str(e)}")
    
    def process_batch(self, prompts: List[str]) -> List[Tuple[str, str]]:
        import time
        start = time.time()
        
        futures = [self.executor.submit(self._process_single, p) for p in prompts]
        results = [f.result() for f in futures]
        
        elapsed = time.time() - start
        print(f"Batch complete: {len(prompts)} queries in {elapsed:.2f}s")
        
        return results

Inference Parameter Tuning

Gemma 4 output quality depends heavily on sampling parameters.

from dataclasses import dataclass
 
@dataclass
class InferenceConfig:
    """Recommended inference parameter sets by use case"""
    
    CONFIGS = {
        "precise": {
            "top_p": 0.1,
            "top_k": 5,
            "temperature": 0.3,
            "description": "Fact-based answers (FAQ, data extraction)"
        },
        "balanced": {
            "top_p": 0.9,
            "top_k": 40,
            "temperature": 0.7,
            "description": "General question-answering"
        },
        "creative": {
            "top_p": 0.95,
            "top_k": 50,
            "temperature": 1.0,
            "description": "Creative writing, brainstorming"
        }
    }
    
    @classmethod
    def get_config(cls, mode: str = "balanced") -> dict:
        if mode not in cls.CONFIGS:
            raise ValueError(f"Invalid mode: {mode}")
        config = cls.CONFIGS[mode]
        description = config.pop("description")
        print(f"Mode: {mode} ({description})")
        return config

Pre-Production Checklist

Before deploying Gemma 4 + Antigravity to production:

  • [ ] Memory capacity confirmed at 13GB+
  • [ ] Caching implemented with 30%+ hit rate
  • [ ] Batch processing reducing multi-query time by 40%+
  • [ ] API keys loaded from environment
  • [ ] Rate limiting implemented with backoff
  • [ ] Sampling parameters tuned per use case
  • [ ] Exception handling with fallback paths
  • [ ] Inference latency < 2s (uncached)
  • [ ] Memory usage stays under 13GB
  • [ ] Logging captures inference results

On-Device Implementation with Android AICore

For on-device Gemma 4 execution on Android, Google's AICore runtime is a viable path. This section covers the concrete implementation for invoking fine-tuned models from agents, plus the hybrid strategy with cloud APIs.

Core AICore Implementation

Step 1: Check Model Availability

// AICoreManager.kt
import com.google.android.aicore.AICore
import com.google.android.aicore.GenerativeModel
import com.google.android.aicore.AvailabilityStatus
 
class AICoreManager(private val context: Context) {
    
    /**
     * Check if Gemma 4 is available on this device
     * @return Calls onAvailable with model, or onUnavailable with reason
     */
    fun checkAvailability(
        onAvailable: (GenerativeModel) -> Unit,
        onUnavailable: (String) -> Unit
    ) {
        AICore.checkAvailability(
            context = context,
            modelName = "gemma-4-e2b-it"  // Edge 2B Instruction Tuned
        ).addOnSuccessListener { status ->
            when (status) {
                AvailabilityStatus.AVAILABLE -> {
                    // Model already downloaded
                    initModel(onAvailable, onUnavailable)
                }
                AvailabilityStatus.DOWNLOADABLE -> {
                    // Download needed
                    downloadAndInit(onAvailable, onUnavailable)
                }
                AvailabilityStatus.NOT_SUPPORTED -> {
                    onUnavailable("This device doesn't support AICore")
                }
            }
        }.addOnFailureListener { e ->
            onUnavailable("Availability check failed: ${e.message}")
        }
    }
    
    private fun initModel(
        onAvailable: (GenerativeModel) -> Unit,
        onUnavailable: (String) -> Unit
    ) {
        AICore.getGenerativeModel(
            modelName = "gemma-4-e2b-it",
            context = context
        ).addOnSuccessListener { model ->
            onAvailable(model)
        }.addOnFailureListener { e ->
            onUnavailable("Model initialization failed: ${e.message}")
        }
    }
}

Step 2: On-Device AI Agent Implementation

// OnDeviceAIAgent.kt
import com.google.android.aicore.GenerativeModel
import com.google.android.aicore.Content
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
 
class OnDeviceAIAgent(private val model: GenerativeModel) {
    
    private val conversationHistory = mutableListOf<Content>()
    
    // System prompt defining agent behavior
    private val systemPrompt = """
        You are a helpful assistant.
        Answer user questions concisely.
        Do not handle sensitive personal information.
    """.trimIndent()
    
    /**
     * Generate text (suspend function, coroutine-compatible)
     */
    suspend fun generate(userMessage: String): String = 
        suspendCancellableCoroutine { continuation ->
            
            // Add user message to history
            conversationHistory.add(
                Content.Builder()
                    .setRole("user")
                    .addText(userMessage)
                    .build()
            )
            
            // Execute on-device inference with AICore
            model.generateContent(conversationHistory)
                .addOnSuccessListener { response ->
                    val text = response.text ?: ""
                    
                    // Add AI response to history
                    conversationHistory.add(
                        Content.Builder()
                            .setRole("model")
                            .addText(text)
                            .build()
                    )
                    
                    continuation.resume(text)
                }
                .addOnFailureListener { e ->
                    continuation.resumeWithException(e)
                }
        }
    
    /**
     * Streaming generation (real-time response)
     */
    fun generateStream(userMessage: String): Flow<String> = flow {
        model.generateContentStream(
            listOf(
                Content.Builder()
                    .setRole("user")
                    .addText(userMessage)
                    .build()
            )
        ).forEach { chunk ->
            chunk.text?.let { emit(it) }
        }
    }
    
    fun clearHistory() {
        conversationHistory.clear()
    }
}

Step 3: ViewModel Integration

// AIAgentViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
 
class AIAgentViewModel : ViewModel() {
    
    private var agent: OnDeviceAIAgent? = null
    
    private val _uiState = MutableStateFlow<AgentUiState>(AgentUiState.Initializing)
    val uiState: StateFlow<AgentUiState> = _uiState
    
    private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList())
    val messages: StateFlow<List<ChatMessage>> = _messages
    
    fun initialize(context: Context) {
        AICoreManager(context).checkAvailability(
            onAvailable = { model ->
                agent = OnDeviceAIAgent(model)
                _uiState.value = AgentUiState.Ready
            },
            onUnavailable = { reason ->
                // On-device unavailable → fall back to cloud API
                _uiState.value = AgentUiState.FallingBackToCloud(reason)
            }
        )
    }
    
    fun sendMessage(text: String) {
        viewModelScope.launch {
            _messages.value += ChatMessage(role = "user", text = text)
            _uiState.value = AgentUiState.Generating
            
            try {
                // Execute on-device inference
                val response = agent?.generate(text) ?: "Error: Agent not initialized"
                _messages.value += ChatMessage(role = "assistant", text = response)
                _uiState.value = AgentUiState.Ready
            } catch (e: Exception) {
                _uiState.value = AgentUiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}
 
sealed class AgentUiState {
    object Initializing : AgentUiState()
    object Ready : AgentUiState()
    object Generating : AgentUiState()
    data class FallingBackToCloud(val reason: String) : AgentUiState()
    data class Error(val message: String) : AgentUiState()
}
 
data class ChatMessage(val role: String, val text: String)

Hybrid Cloud + On-Device Strategy

Not all devices support AICore, so a fallback strategy is essential:

// HybridAIStrategy.kt
class HybridAIStrategy(
    private val onDeviceAgent: OnDeviceAIAgent?,
    private val cloudApi: GeminiApiClient
) {
    suspend fun generate(prompt: String): String {
        return if (onDeviceAgent != null) {
            // On-device first (fast, free, private)
            try {
                onDeviceAgent.generate(prompt)
            } catch (e: Exception) {
                // Fall back to cloud on failure
                cloudApi.generate(prompt)
            }
        } else {
            // Device unsupported: use cloud API
            cloudApi.generate(prompt)
        }
    }
}

Also see Android Studio + Antigravity Development Guide for setting up the full Android development environment.

Looking back

Gemma 4's on-device AI integration has reached a stage where a consistent workflow from prototype to production is fully achievable. By combining the hybrid architecture design, LoRA fine-tuning for custom models, Antigravity agent integration, and CI/CD pipeline automation covered in this article, you can build competitive products centered on edge AI.

Start with the area that will have the most impact on your project. For deeper exploration of edge device AI experiences, see Antigravity × Core ML: Complete Guide to iOS On-Device AI Development. If you want to combine this with AgentKit 2.0's multi-agent capabilities, Antigravity AgentKit 2.0 Multi-Agent Development Practical Guide is an excellent companion resource.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

AI Tools2026-04-11
How to Accelerate App Development with Gemma 4 and Antigravity
Learn how to combine Google DeepMind's open model Gemma 4 with Antigravity for app development. A beginner-friendly guide covering model selection, setup, and practical integration.
Integrations2026-05-15
Using Claude Opus 4 / Sonnet 4 in Antigravity — Model Selection Strategy and Production Patterns
A practical guide to using Claude Opus 4, Sonnet 4, and Haiku 4.5 in Antigravity. Learn the decision framework and production implementation patterns for balancing cost, speed, and quality in real projects.
Integrations2026-04-30
Stateful AI Agents on Antigravity × Cloudflare Durable Objects — One Session, One Instance, One Place to Manage Conversation, Tools, and Cost
How to deploy an Antigravity AgentKit 2.0 agent onto Cloudflare Durable Objects so that conversation history, tool-call state, and token spend live inside a single instance per session — with the actual code and production cost numbers from my own deployment.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →