GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-05-15Intermediate

I Rebuilt My Wallpaper App's Recommendation Engine Using Gemini Function Calling

A hands-on account of integrating Gemini Function Calling into a wallpaper app with 50M+ downloads. Covers schema design, cost estimation, and how I compared Gemini against Claude and GPT-4o for this use case.

gemini-api277function-calling20indie-dev43recommendation-enginewallpaper-app5

By late 2025, I realized the recommendation logic in my wallpaper app had become purely inertial.

I've been building and running wallpaper apps as an indie developer since 2014, and after crossing 50 million total downloads, the core logic for predicting "what users want to see next" hadn't really changed. It was a combination of tag-based filtering and popularity ranking by download count — simple, stable, but clearly limited when it came to personalizing for individual users.

The push to try Function Calling came after I verified the cost-performance of Gemini 2.5 Flash. Not just "can it classify or tag things," but something more specific: could I realistically pass user behavior logs as structured data and have the model infer preferences? I decided to find out.

How a 12-Year-Old Pattern Finally Got Noticed

Solo-developed apps tend to reach a "it's working, don't touch it" state quickly. Backend logic especially — users don't see it directly, so it gets deprioritized.

What prompted me this time was reading through recent user reviews. A few mentioned that recommendations "always look the same." When I checked the data, top-downloaded wallpapers were repeatedly surfacing for the same users, while wallpapers in categories they'd recently downloaded from weren't being prioritized.

Rewriting the rules wasn't technically hard. The real question was: what rules? User preferences are diverse enough that broad categories like nature, city, abstract, and animal aren't granular enough. Managing fine-grained tags manually at scale isn't realistic either. That's where the Function Calling idea surfaced.

Why Function Calling — Compared to the Alternatives

My first attempt was simple: dump user behavior logs into a prompt and ask "what should I show next?" The output was unstable enough that parsing it reliably on the app side was a real problem.

Structured Output (JSON mode) was more stable, but it meant the model only received data I explicitly passed upfront. For recommendations, you need three things to make a good call: what's currently available, what the user has downloaded recently, and which wallpapers they spent the most time viewing. Having the model ask for what it needs, rather than receiving everything upfront, felt like the right design.

With Function Calling, the model can actively request "give me this user's download history" or "what's available in the abstract category right now." That kind of dynamic, on-demand querying matched the recommendation pattern well.

Claude 3.7 Sonnet and GPT-4o can do the same thing, but recommendation calls can number in the hundreds to thousands per session. Gemini 2.5 Flash's input cost runs roughly 1/10th of Claude Sonnet (as of my May 2026 estimates), and at high call volumes, that gap compounds quickly. For throughput-heavy, cost-sensitive processing where precision can be somewhat traded for speed, Flash made sense.

For caching strategies that push costs down further, see Gemini API Context Caching — 80% Cost Reduction in Production.

Schema Design — How to Pass User Behavior

Schema design was where I spent the most time. The key question: which data goes into which function?

I landed on three function declarations:

  • get_user_download_history: past 30 days of downloads (wallpaper ID, category, timestamp)
  • get_user_view_behavior: list of wallpaper IDs where the user lingered longest (calculated from scroll stop time)
  • get_available_wallpapers: currently available wallpapers (category, tags, popularity score)

I initially tried passing everything through a single large function, but having the model request only what it needs produces fewer unnecessary tokens. get_available_wallpapers in particular can return a lot of data for large catalogs, so I added a category argument to return a filtered subset.

import google.generativeai as genai
 
# Function schema definitions
tools = [
    {
        "function_declarations": [
            {
                "name": "get_user_download_history",
                "description": "Retrieve the user's wallpaper download history for the past 30 days",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "user_id": {
                            "type": "string",
                            "description": "The user's identifier"
                        },
                        "limit": {
                            "type": "integer",
                            "description": "Maximum number of records to return (default: 20)"
                        }
                    },
                    "required": ["user_id"]
                }
            },
            {
                "name": "get_available_wallpapers",
                "description": "Retrieve currently available wallpapers, optionally filtered by category",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "category": {
                            "type": "string",
                            "description": "Category filter (nature/city/abstract/animal, etc.)"
                        },
                        "limit": {
                            "type": "integer",
                            "description": "Maximum number of records to return (default: 50)"
                        }
                    },
                    "required": []
                }
            }
        ]
    }
]
 
# Function Calling execution loop
def run_recommendation(user_id: str) -> list[str]:
    """Pass a user ID and return a list of recommended wallpaper IDs."""
    model = genai.GenerativeModel("gemini-2.5-flash-preview-05-20")
    
    chat = model.start_chat()
    response = chat.send_message(
        f"Recommend 5 wallpapers for user {user_id}. "
        f"Use the available functions to gather the data you need before deciding. "
        f"Return only a JSON array of wallpaper IDs as your final answer.",
        tools=tools
    )
    
    # Function Calling loop — model requests data as needed
    while response.candidates[0].content.parts[0].function_call.name:
        fc = response.candidates[0].content.parts[0].function_call
        func_name = fc.name
        func_args = dict(fc.args)
        
        # Fetch actual data from local DB
        if func_name == "get_user_download_history":
            result = fetch_download_history(func_args.get("user_id"), func_args.get("limit", 20))
        elif func_name == "get_available_wallpapers":
            result = fetch_wallpapers(func_args.get("category"), func_args.get("limit", 50))
        else:
            result = {}
        
        response = chat.send_message(
            genai.protos.Content(
                parts=[genai.protos.Part(
                    function_response=genai.protos.FunctionResponse(
                        name=func_name,
                        response={"result": result}
                    )
                )]
            )
        )
    
    # Extract ID list from final output
    import json
    raw = response.text.strip()
    return json.loads(raw)

fetch_download_history and fetch_wallpapers query my own database. The key behavior here: the model decides autonomously which functions to call and in what order. In practice, I observed it almost always calling get_user_download_history first, followed by get_available_wallpapers with a specific category argument.

Three Things I Learned from Actually Running It

1. Handle the "model skips the function call" case

For first-time users with no download history, the model occasionally skipped get_user_download_history and jumped straight to recommending. Adding an explicit instruction — "always call get_user_download_history first" — resolved it. This is a Function Calling-specific footgun worth knowing about. See Troubleshooting Gemini Function Calling Issues for a more thorough rundown.

2. Field names in the schema directly affect accuracy

wallpaper_id performed better than just id in my testing — the model seemed to handle it more consistently when the name was self-explanatory. The same logic applies to type labels: using string for IDs worked better than integer, even when the IDs are numeric. Name your schema fields the way you'd explain them to a new developer.

3. Parallel calls are necessary at scale

Sequential processing per user didn't scale to the volumes I needed. Gemini API supports parallel Function Calling, so in batch processing I now run multiple users concurrently. For patterns on that approach, see Parallel Function Calling Production Patterns with Gemini API.

How I Settled the Claude vs. OpenAI vs. Gemini Decision

This project clarified my working heuristic for model selection, so I'll write it out.

For high-frequency, cost-sensitive processing — recommendations, classification, scoring — Gemini 2.5 Flash is the practical default for me now. The more calls you make, the more the cost gap matters. I estimate call volume before choosing a model, not after.

For quality-critical tasks — long-form responses to users, complex multi-step reasoning — Gemini 2.5 Pro and Claude Sonnet produce more reliable results. At indie dev scale, designing around "use premium models sparingly" is how I keep monthly costs predictable.

OpenAI's Function Calling spec is stable and the error-handling tooling around it is mature. I still reference GPT-4o implementations when prototyping something new, but Gemini Flash has become the production default for cost-heavy workloads.

Where to Go from Here

If you're starting from scratch, the lowest-friction entry point is to take one existing rule-based logic component, wrap it as a function, and let the model make the call. You don't need to replace everything at once — and comparing the model's outputs against your existing rules before switching is a useful sanity check.

For anyone running an app with similar recommendation needs, I hope this account of the design decisions and trade-offs is useful as a starting point.

Share

Thank You for Reading

Gemini 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

API / SDK2026-06-23
Integrating Gemini 3.2 Pro Function Calling into iOS/Android Apps: Production Design Patterns
A practical guide to integrating Gemini 3.2 Pro Function Calling into iOS and Android apps. Includes working SwiftUI, Kotlin, and Python code, plus production patterns proven in a real indie wallpaper app — cost, latency, staged rollout, and regression testing.
API / SDK2026-05-26
Pairing Gemini API with Apple FoundationModels (iOS 26): An On-Device-First Hybrid Routing Notebook
Running iOS 26 FoundationModels alongside Gemini API as a hybrid stack for a wallpaper app's poem-from-image feature: routing decisions, full Swift code, and one week of latency and cost numbers.
API / SDK2026-05-18
Building a Wallpaper Variation Pipeline with Gemini 3.2 Flash Image Output — How an Indie Developer Splits the Work with Imagen 4 and Cut Monthly API Cost
An indie developer's working notes on combining Gemini 3.2 Flash Image Output with Imagen 4 to power a wallpaper-variation feature. Includes Python code, cost numbers, and three production traps from running wallpaper apps with 50M+ downloads since 2014.
📚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 →