28 October 2026 · 8 min read

Fine-tuning FunctionGemma for BMONI's on-device agent

Training FunctionGemma 270M for wallet intent routing, testing it on an iPhone, and carrying the results into BMONI chat flows.

“Request 100 naira from the group wallet for snacks” sounds straightforward until an app has to act on it. It needs to recognise a request, extract the amount, identify the group, and carry the reason into a confirmation screen. Confusing that with sending money would prepare the wrong action.

That is the problem I worked on in BMONI, a multi-currency wallet built with Flutter. I adapted FunctionGemma 270M to route messages into the app's capabilities, then connected that routing to wallet screens and conversations. The model proposes a capability and its parameters. The app gathers missing information, applies policy, and asks for confirmation before executing an action.

A small model with a bounded job

I started with Google's pretrained google/functiongemma-270m-it. Training here means fine-tuning an existing model for BMONI's vocabulary and tools. I was not pretraining a foundation model. FunctionGemma's model card describes its function-calling purpose and intended adaptation through fine-tuning.

BMONI already had capabilities for balances, transfers, money requests, exchange rates, and group wallets. I used those interfaces as the model's targets. That kept its output close to something the app could validate.

In this example, the message is sent from a fictional group called Weekend group, so the group name comes from chat context. This is a conceptual tool call for the opening message:

{
  "capability": "groups",
  "slots": {
    "group": "Weekend group",
    "action": "request"
  }
}

It illustrates routing, not the complete executable request. Amount, currency, and reason still have to survive the rest of the flow. A plausible tool name alone is not enough.

The working stack

The latest training round ran locally on my Mac using Apple MPS. The script also supports CUDA and a Colab workflow, but those were not the machines behind the results below.

Layer What I used
Training Python 3.12, PyTorch with Apple MPS, Hugging Face Transformers, Datasets, and TRL's SFTTrainer
Conversion and inference litert-torch export to .litertlm, then LiteRT-LM
Mobile app Flutter/Dart, flutter_edge_ai 2.4.0, flutter_edge_ai_litertlm 1.14.0, Riverpod
Conversations Stream Chat Flutter 9.24.0 for direct and group chats
Artifact delivery Hugging Face Hub artifacts; Doppler configuration for version, URL, checksum, and size
Development tooling Claude Code for implementation and debugging; iMirror for physical-device inspection

I used Claude Code while implementing and debugging the pipeline, with iMirror providing the physical-device interaction loop.

The working path from data to the app was:

Synthetic examples -> full fine-tune -> held-out evaluation
  -> LiteRT export -> device evaluation -> app integration checks

Training on the prompt the phone uses

The latest dataset contained 8,530 synthetic training examples across English, Spanish including Mexican Spanish, French, and Chinese. The examples pair messages with expected tool calls and arguments.

The difficult part was keeping Python and Dart in agreement. The system instruction, tool declarations, and message format used for training mirror the app's capability manifest and intent extractor. A parity fixture checks that relationship.

The entire capability registry does not fit comfortably into the model's context budget. The phone ranks and selects tools for each turn, with group context and an active flow affecting priority. Training has to reproduce that selection. Otherwise, the model learns with tools that may be absent when a user asks the same question on the phone.

This pipeline uses a 1,024-token context and a maximum of 128 generated tokens. A routing failure can therefore begin before generation: if the expected tool was excluded, the model cannot call it.

I excluded exact held-out evaluation utterances from training. That prevents literal overlap. It does not establish that every training example is semantically different from every evaluation case. I keep that distinction in mind when reading the scores.

Full fine-tuning on the Mac

This run used full fine-tuning, without LoRA or QLoRA. I applied loss only to the completion so that the objective focuses on the answer tokens. TRL documents this behaviour through its completion-only loss configuration.

The run used an effective batch size of 32 through gradient accumulation, a cosine learning-rate schedule, and gradient checkpointing. It saved checkpoints every 100 steps. One epoch took 267 steps, with a logged training runtime of 7,450.96 seconds, about two hours and four minutes. That is an observation from this local run, not a speed benchmark for other hardware.

Some less visible work mattered too. I padded sequence lengths to multiples of 64 and cleared the MPS cache to manage memory. When loading examples, I formatted Python objects directly to avoid Arrow introducing null argument fields into tool calls. Those nulls would have changed the answers the model was being taught to produce.

I separated the in-training evaluation split from the held-out routing benchmark. A falling training loss told me the optimiser was progressing. It did not tell me whether the app would prepare the right request.

What the evaluation actually measured

On October 28, 2026, the results looked like this:

Evaluation path Tool-and-slot accuracy
Mac held-out benchmark 330/350, 94.3%
iPhone Air, model only 325/350, 92.9%
iPhone Air, path with shortcuts 324/350, 92.6%

These scores measure the selected tool and extracted parameters. They do not measure successful transfers or production reliability. The phone path with shortcuts also produced one incorrect request that reached confirmation. That is worth tracking separately from the overall accuracy percentage. Reaching a confirmation card is not evidence that money moved.

The shortcuts path scored slightly below the model alone. A shortcut can bypass a good model prediction or introduce a parsing error, so I need to evaluate the combined path as well as each component.

Failures made the next steps concrete. An ambiguous dollar symbol in “ask a test contact for $1” could become an invented USD assumption. A decimal-comma amount such as 12,50 could become 1250. Other misses involved the expected tool being absent from the prompt. I cannot attribute the desktop-to-phone gap to quantisation alone from these results.

Exporting the artifact the app downloads

After training, I exported the model with the pipeline's default dynamic_wi8_afp32 configuration, which uses int8 weights. The resulting versioned .litertlm artifact was about 285.6 MB in decimal units. LiteRT-LM provides the runtime background for this deployment path.

Conversion needed a separate environment: the training pipeline pinned Transformers 4.57.1, while the converter setup used Transformers 5.15.1 with litert-torch 0.9.4. Keeping those environments separate avoided changing the training dependencies just to produce the mobile artifact.

The app validates the download's SHA-256 and expected size. Each release needs a unique version because the app caches the model by filename. Reusing a version can leave a device testing an older artifact while the developer thinks it has the new one.

The latest configured development and staging artifact was hosted through Hugging Face. The repository also supports a Firebase Storage mirror. Local inference handles intent extraction; balances and financial actions still depend on the app's services.

Checking the conversation around the model

I integrated private @bmoniagent interactions into direct and group chats, intercepting them before ordinary chat transmission. An action that needs the dedicated agent screen hands off with its group identity intact. Losing that context can turn an otherwise correct request into another “which group?” question.

I used iMirror to walk these flows on the physical phone and inspect the resulting cards. The BMONI case study includes screenshots with personal data blurred. Those checks complemented the benchmark: they exposed whether a correct intent survived navigation, cancellation, and confirmation.

The next training round needs realistic ambiguity cases, especially currency symbols and decimal separators. I also want the wrong-confirmation count visible beside routing accuracy. Prompt parity and a desktop score are useful checks, but I still need to run the downloaded artifact through the phone's actual routing path and inspect what the user is asked to approve.

© 2026 Theophilus Rex Danquah. All rights reserved.