GEMINI LABJP
ROBOTICS — The ER 1.6 preview that shut down on August 31 does have a successor. Gemini Robotics ER 2 is in public preview, in both standard and streaming variantsVIDEO — ER 2 judges success and failure from live video rather than still snapshots, which is what lets it catch spills, slips, and misalignments while a task is still runningDEADLINE — Next up is September 30, when gemini-omni-flash-preview is retired. The target is gemini-omni-1.1-flash, GA since August 27, and there are now under four weeks leftAPIKEY — Every remaining standard API key, restricted ones included, stops working during September. The replacement is an auth key bound to a Google Cloud service accountPRICE — Gemini 3.7 Flash keeps its introductory $0.75/$3.75 per 1M through December 31, then moves to $1.50/$7.50 on January 1, 2027. Any estimate crossing the year needs both figuresAUDIO — Gemini 3.5 Transcribe handles language detection across 85+ languages, speaker diarization, word-level timestamps, and custom vocabulary biasing of up to 1,000 termsROBOTICS — The ER 1.6 preview that shut down on August 31 does have a successor. Gemini Robotics ER 2 is in public preview, in both standard and streaming variantsVIDEO — ER 2 judges success and failure from live video rather than still snapshots, which is what lets it catch spills, slips, and misalignments while a task is still runningDEADLINE — Next up is September 30, when gemini-omni-flash-preview is retired. The target is gemini-omni-1.1-flash, GA since August 27, and there are now under four weeks leftAPIKEY — Every remaining standard API key, restricted ones included, stops working during September. The replacement is an auth key bound to a Google Cloud service accountPRICE — Gemini 3.7 Flash keeps its introductory $0.75/$3.75 per 1M through December 31, then moves to $1.50/$7.50 on January 1, 2027. Any estimate crossing the year needs both figuresAUDIO — Gemini 3.5 Transcribe handles language detection across 85+ languages, speaker diarization, word-level timestamps, and custom vocabulary biasing of up to 1,000 terms
Articles/API / SDK
API / SDK/2026-03-14Advanced

Fine-Tuning Gemini: Building Specialized Models for Domain-Specific Applications

Complete guide to fine-tuning Gemini models. Learn dataset preparation, training strategies, evaluation metrics, and production deployment for custom AI.

fine-tuning2model-customizationtraininggemini-api283llm-optimization

Fine-Tuning Gemini: Building Specialized Models for Domain-Specific Applications

Off-the-shelf language models excel at general tasks. But when you need a model that understands your domain's language patterns, industry jargon, or specialized reasoning, fine-tuning becomes essential. Gemini's fine-tuning API enables you to build models that perform like experts in your field.

Understanding Fine-Tuning: When and Why

Fine-tuning adjusts a pre-trained model's weights using your data. This is different from prompt engineering or RAG, which work with the model as-is. Fine-tuning permanently changes the model's behavior.

💡
Fine-tuning is most effective when: - You have domain-specific vocabulary (legal documents, medical notes, financial reports) - You need consistent output formatting (structured data extraction, specific JSON schemas) - Your domain has unique reasoning patterns (specialized diagnostic procedures) - You're willing to invest in quality data preparation (at least 100-200 high-quality examples)

It's less effective for tasks better solved by RAG, few-shot prompting, or basic tool integration.

Fine-Tuning vs. Alternatives

# Scenario: Extract structured insights from compliance documents
 
# Option 1: Few-shot prompting (quick, limited)
def extract_with_fewshot(document: str) -> dict:
    response = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1000,
        messages=[{
            "role": "user",
            "content": f"""
            Extract compliance violations from this document.
 
            Example 1:
            Input: "System was down for 4 hours on 2024-03-14"
            Output: {{"violation": "SLA breach", "duration_hours": 4, "date": "2024-03-14"}}
 
            Now extract from: {document}
            """
        }]
    )
    return json.loads(response.content[0].text)
 
# Option 2: RAG with domain docs (retrieval-focused)
def extract_with_rag(document: str) -> dict:
    # Retrieve similar examples from knowledge base
    examples = retrieve_examples(document, top_k=3)
    # Use retrieved examples in prompt
    pass
 
# Option 3: Fine-tuned model (specialized)
def extract_with_finetuned(document: str) -> dict:
    response = client.messages.create(
        model="compliance-extractor-v1",  # Fine-tuned model
        max_tokens=1000,
        messages=[{
            "role": "user",
            "content": f"Extract violations: {document}"
        }]
    )
    return json.loads(response.content[0].text)

For compliance extraction where you process thousands of documents and need consistent, specialized extraction, fine-tuning pays for itself.

Dataset Preparation: The Foundation

Fine-tuning quality depends entirely on your data. Garbage in, garbage out applies here more than anywhere else.

Data Format and Structure

Gemini expects training data in a specific format. Each training example should include:

from dataclasses import dataclass
from typing import List, Optional
import json
 
@dataclass
class TrainingExample:
    """A single training example for fine-tuning."""
    input_text: str
    output_text: str
    metadata: Optional[dict] = None
 
class DatasetPreparer:
    def __init__(self, output_format: str = "jsonl"):
        self.output_format = output_format
        self.examples: List[TrainingExample] = []
 
    def add_example(
        self,
        input_text: str,
        output_text: str,
        metadata: dict = None
    ):
        """Add a training example."""
        example = TrainingExample(
            input_text=input_text,
            output_text=output_text,
            metadata=metadata or {}
        )
        self.examples.append(example)
 
    def validate_examples(self) -> dict:
        """Validate dataset quality before training."""
        validation_report = {
            "total_examples": len(self.examples),
            "avg_input_length": 0,
            "avg_output_length": 0,
            "length_distribution": {},
            "potential_issues": [],
        }
 
        if len(self.examples) < 100:
            validation_report["potential_issues"].append(
                "Dataset smaller than recommended minimum (100 examples)"
            )
 
        input_lengths = []
        output_lengths = []
 
        for example in self.examples:
            input_len = len(example.input_text.split())
            output_len = len(example.output_text.split())
            input_lengths.append(input_len)
            output_lengths.append(output_len)
 
        validation_report["avg_input_length"] = sum(input_lengths) / len(input_lengths)
        validation_report["avg_output_length"] = sum(output_lengths) / len(output_lengths)
 
        # Check for duplicates
        unique_inputs = len(set(e.input_text for e in self.examples))
        if unique_inputs < len(self.examples) * 0.9:
            validation_report["potential_issues"].append(
                f"High duplication rate: {1 - unique_inputs/len(self.examples):.1%}"
            )
 
        # Check for output consistency
        if max(output_lengths) > 4000:
            validation_report["potential_issues"].append(
                "Some outputs exceed recommended max length (4000 tokens)"
            )
 
        return validation_report
 
    def export_training_data(
        self,
        filepath: str,
        include_metadata: bool = True,
        train_split: float = 0.8
    ):
        """
        Export training data in JSONL format for Gemini API.
        Includes automatic train/eval split.
        """
 
        import random
        random.shuffle(self.examples)
 
        split_idx = int(len(self.examples) * train_split)
        train_examples = self.examples[:split_idx]
        eval_examples = self.examples[split_idx:]
 
        # Export training set
        with open(f"{filepath}.train.jsonl", "w") as f:
            for example in train_examples:
                record = {
                    "messages": [
                        {
                            "role": "user",
                            "content": example.input_text,
                        },
                        {
                            "role": "assistant",
                            "content": example.output_text,
                        }
                    ]
                }
                if include_metadata and example.metadata:
                    record["metadata"] = example.metadata
                f.write(json.dumps(record) + "\n")
 
        # Export eval set
        with open(f"{filepath}.eval.jsonl", "w") as f:
            for example in eval_examples:
                record = {
                    "messages": [
                        {
                            "role": "user",
                            "content": example.input_text,
                        },
                        {
                            "role": "assistant",
                            "content": example.output_text,
                        }
                    ]
                }
                if include_metadata and example.metadata:
                    record["metadata"] = example.metadata
                f.write(json.dumps(record) + "\n")
 
        return {
            "train_examples": len(train_examples),
            "eval_examples": len(eval_examples),
            "train_file": f"{filepath}.train.jsonl",
            "eval_file": f"{filepath}.eval.jsonl",
        }
 
# Real-world example: Legal document analysis
preparer = DatasetPreparer()
 
# Example 1: Contract analysis
preparer.add_example(
    input_text="This Agreement is made effective as of January 1, 2024. "
               "The Licensor grants a non-exclusive, worldwide license to use the Software "
               "for internal business purposes only. All warranty is disclaimed.",
    output_text=json.dumps({
        "contract_type": "Software License",
        "effective_date": "2024-01-01",
        "scope": "non-exclusive, worldwide",
        "usage": "internal business purposes",
        "warranty": "disclaimed",
        "key_clauses": ["license grant", "warranty disclaimer"]
    }),
    metadata={"document_id": "contract_001", "category": "license"}
)
 
# Example 2: Risk identification
preparer.add_example(
    input_text="Liability shall be limited to direct damages not exceeding the total fees "
               "paid in the prior 12 months. Neither party shall be liable for indirect, "
               "incidental, or consequential damages.",
    output_text=json.dumps({
        "liability_cap": "direct damages only",
        "cap_amount": "total fees (12 months)",
        "excluded_damages": ["indirect", "incidental", "consequential"],
        "risk_level": "low"
    }),
    metadata={"document_id": "contract_001", "category": "liability"}
)
 
# Validate and export
report = preparer.validate_examples()
print("Validation Report:", report)
 
export_info = preparer.export_training_data(
    "legal_analysis_dataset",
    train_split=0.8
)
print("Export Info:", export_info)
⚠️
Data quality directly impacts model quality. Spend time: - Removing duplicates - Fixing inconsistent formatting - Validating output correctness - Ensuring examples are representative - Checking for biases or artifacts

A smaller, cleaner dataset often beats a larger, noisier one.

Training Configuration and Hyperparameters

Once your data is ready, configure the training job. Gemini exposes key hyperparameters:

import anthropic
import json
from typing import Optional
 
class GeminiFineTuner:
    def __init__(self, api_key: str = None):
        self.client = anthropic.Anthropic(api_key=api_key)
 
    def create_fine_tuning_job(
        self,
        training_file_path: str,
        eval_file_path: str,
        model_id: str = "compliance-extractor-v1",
        display_name: str = "Compliance Document Extractor",
        learning_rate: float = 0.001,
        num_epochs: int = 3,
        batch_size: int = 32,
        weight_decay: float = 0.01,
        warmup_steps: int = 100,
    ) -> dict:
        """
        Create a fine-tuning job with specified hyperparameters.
 
        Args:
            training_file_path: Path to JSONL training data
            eval_file_path: Path to JSONL evaluation data
            model_id: Identifier for the fine-tuned model
            learning_rate: Learning rate for optimization
            num_epochs: Number of training epochs
            batch_size: Batch size for training
            weight_decay: L2 regularization strength
            warmup_steps: Steps for learning rate warmup
 
        Returns:
            Job metadata including job_id and status
        """
 
        # Upload training data
        with open(training_file_path, "rb") as f:
            train_response = self.client.beta.files.upload(
                file=f,
            )
        train_file_id = train_response.id
 
        # Upload eval data
        with open(eval_file_path, "rb") as f:
            eval_response = self.client.beta.files.upload(
                file=f,
            )
        eval_file_id = eval_response.id
 
        # Create fine-tuning job
        job_response = self.client.beta.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=1024,
            messages=[
                {
                    "role": "user",
                    "content": (
                        f"Create a fine-tuning job with the following parameters:\n"
                        f"Model ID: {model_id}\n"
                        f"Display Name: {display_name}\n"
                        f"Training File: {train_file_id}\n"
                        f"Eval File: {eval_file_id}\n"
                        f"Learning Rate: {learning_rate}\n"
                        f"Epochs: {num_epochs}\n"
                        f"Batch Size: {batch_size}\n"
                        f"Weight Decay: {weight_decay}\n"
                        f"Warmup Steps: {warmup_steps}"
                    ),
                }
            ],
        )
 
        return {
            "job_id": model_id,
            "display_name": display_name,
            "training_file_id": train_file_id,
            "eval_file_id": eval_file_id,
            "hyperparameters": {
                "learning_rate": learning_rate,
                "num_epochs": num_epochs,
                "batch_size": batch_size,
                "weight_decay": weight_decay,
                "warmup_steps": warmup_steps,
            },
            "status": "submitted",
        }
 
    def monitor_training(self, job_id: str) -> dict:
        """
        Monitor training job progress.
        Returns metrics and current status.
        """
 
        # Implementation would poll the training API
        # and return progress metrics
        return {
            "job_id": job_id,
            "status": "training",
            "epoch": 2,
            "total_epochs": 3,
            "loss": 0.245,
            "eval_loss": 0.312,
            "estimated_time_remaining": 3600,
        }

Hyperparameter Tuning Strategy

class HyperparameterOptimizer:
    def __init__(self, base_model: str = "gemini-pro"):
        self.base_model = base_model
        self.trials = []
 
    def grid_search(
        self,
        training_file: str,
        eval_file: str,
        param_grid: dict,
    ) -> list:
        """
        Run multiple training jobs with different hyperparameters.
        Useful for finding optimal settings for your domain.
        """
 
        trials = []
 
        for learning_rate in param_grid.get("learning_rate", [0.001]):
            for batch_size in param_grid.get("batch_size", [32]):
                for num_epochs in param_grid.get("num_epochs", [3]):
                    trial = {
                        "learning_rate": learning_rate,
                        "batch_size": batch_size,
                        "num_epochs": num_epochs,
                        "status": "pending",
                    }
                    trials.append(trial)
 
        # Submit all trials
        for trial in trials:
            # Submit training job
            pass
 
        return trials
 
    def get_best_hyperparameters(
        self,
        metric: str = "eval_f1"
    ) -> dict:
        """
        Analyze completed trials and return best hyperparameters.
        """
 
        completed = [t for t in self.trials if t["status"] == "completed"]
 
        if not completed:
            return None
 
        best = max(completed, key=lambda t: t.get(metric, 0))
        return best

Evaluation: Measuring Model Quality

Before deploying, rigorously evaluate your fine-tuned model. Generic metrics (loss, accuracy) matter less than domain-specific metrics.

💡
Choose evaluation metrics that match your business goals. For legal document analysis, you might care about: - Precision of clause extraction (false positives are costly) - Recall of risk indicators (missing risks is dangerous) - Consistency across document types - Human reviewer alignment
from typing import List, Dict
import json
from dataclasses import dataclass
 
@dataclass
class EvaluationMetrics:
    precision: float
    recall: float
    f1: float
    accuracy: float
    custom_metrics: Dict[str, float] = None
 
class ModelEvaluator:
    def __init__(self, model_id: str, client):
        self.model_id = model_id
        self.client = client
        self.predictions = []
        self.ground_truth = []
 
    def evaluate_on_dataset(
        self,
        eval_file_path: str,
        extraction_task: bool = True,
    ) -> EvaluationMetrics:
        """
        Evaluate model on evaluation dataset.
        Supports structured extraction and classification tasks.
        """
 
        # Load evaluation data
        eval_examples = []
        with open(eval_file_path, "r") as f:
            for line in f:
                eval_examples.append(json.loads(line))
 
        predictions = []
        ground_truth = []
 
        # Generate predictions
        for example in eval_examples[:100]:  # Sample for cost
            messages = example.get("messages", [])
            user_message = next(
                (m for m in messages if m["role"] == "user"),
                None
            )
            expected_response = next(
                (m for m in messages if m["role"] == "assistant"),
                None
            )
 
            if not user_message or not expected_response:
                continue
 
            # Get model prediction
            response = self.client.messages.create(
                model=self.model_id,
                max_tokens=1000,
                messages=[{"role": "user", "content": user_message["content"]}],
            )
 
            prediction = response.content[0].text
            predictions.append(prediction)
            ground_truth.append(expected_response["content"])
 
        # Calculate metrics
        if extraction_task:
            metrics = self._evaluate_extraction(predictions, ground_truth)
        else:
            metrics = self._evaluate_classification(predictions, ground_truth)
 
        return metrics
 
    def _evaluate_extraction(
        self,
        predictions: List[str],
        ground_truth: List[str]
    ) -> EvaluationMetrics:
        """
        Evaluate structured extraction tasks.
        Parses JSON and measures field accuracy.
        """
 
        correct_extractions = 0
        total_fields = 0
        extracted_fields = 0
 
        for pred, truth in zip(predictions, ground_truth):
            try:
                pred_json = json.loads(pred)
                truth_json = json.loads(truth)
 
                # Count matching fields
                for key in truth_json:
                    total_fields += 1
                    if key in pred_json and pred_json[key] == truth_json[key]:
                        correct_extractions += 1
                    extracted_fields += 1
 
            except json.JSONDecodeError:
                continue
 
        precision = correct_extractions / extracted_fields if extracted_fields > 0 else 0
        recall = correct_extractions / total_fields if total_fields > 0 else 0
        f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
 
        return EvaluationMetrics(
            precision=precision,
            recall=recall,
            f1=f1,
            accuracy=correct_extractions / total_fields if total_fields > 0 else 0,
            custom_metrics={
                "correct_extractions": correct_extractions,
                "total_fields": total_fields,
                "extraction_rate": extracted_fields / len(predictions) if predictions else 0,
            }
        )
 
    def _evaluate_classification(
        self,
        predictions: List[str],
        ground_truth: List[str]
    ) -> EvaluationMetrics:
        """Evaluate classification tasks."""
 
        correct = sum(1 for p, t in zip(predictions, ground_truth) if p.strip() == t.strip())
        total = len(predictions)
        accuracy = correct / total if total > 0 else 0
 
        return EvaluationMetrics(
            precision=accuracy,
            recall=accuracy,
            f1=accuracy,
            accuracy=accuracy,
        )
 
    def error_analysis(
        self,
        predictions: List[str],
        ground_truth: List[str],
    ) -> dict:
        """
        Identify systematic errors in model predictions.
        Helps understand where fine-tuning failed.
        """
 
        error_patterns = {
            "missing_fields": [],
            "incorrect_values": [],
            "formatting_issues": [],
            "hallucinations": [],
        }
 
        for pred, truth in zip(predictions, ground_truth):
            try:
                pred_json = json.loads(pred)
                truth_json = json.loads(truth)
 
                # Check for missing fields
                missing = set(truth_json.keys()) - set(pred_json.keys())
                if missing:
                    error_patterns["missing_fields"].append({
                        "missing": list(missing),
                        "prediction": pred_json,
                    })
 
                # Check for incorrect values
                for key in truth_json:
                    if key in pred_json and pred_json[key] != truth_json[key]:
                        error_patterns["incorrect_values"].append({
                            "field": key,
                            "expected": truth_json[key],
                            "got": pred_json[key],
                        })
 
            except json.JSONDecodeError:
                error_patterns["formatting_issues"].append({
                    "prediction": pred,
                })
 
        return error_patterns

Production Deployment

Once satisfied with evaluation results, deploy the model with proper versioning and monitoring.

class FineTunedModelDeployment:
    def __init__(self, model_id: str, client):
        self.model_id = model_id
        self.client = client
        self.deployment_config = {}
 
    def prepare_for_production(
        self,
        fallback_model: str = "claude-3-5-sonnet-20241022",
        max_retries: int = 3,
        timeout_seconds: int = 30,
    ) -> dict:
        """
        Prepare model for production deployment.
        Includes fallback strategy and error handling.
        """
 
        self.deployment_config = {
            "primary_model": self.model_id,
            "fallback_model": fallback_model,
            "max_retries": max_retries,
            "timeout_seconds": timeout_seconds,
            "health_check_interval": 3600,
            "enable_monitoring": True,
            "enable_logging": True,
        }
 
        return self.deployment_config
 
    def call_with_fallback(
        self,
        user_message: str,
        **kwargs
    ) -> str:
        """
        Call the fine-tuned model with automatic fallback.
        If the primary model fails, falls back to base model.
        """
 
        for attempt in range(self.deployment_config["max_retries"]):
            try:
                response = self.client.messages.create(
                    model=self.deployment_config["primary_model"],
                    messages=[{"role": "user", "content": user_message}],
                    timeout=self.deployment_config["timeout_seconds"],
                    **kwargs
                )
                return response.content[0].text
 
            except Exception as e:
                if attempt < self.deployment_config["max_retries"] - 1:
                    continue
 
                # Fallback to base model
                print(f"Primary model failed: {e}. Falling back...")
                response = self.client.messages.create(
                    model=self.deployment_config["fallback_model"],
                    messages=[{"role": "user", "content": user_message}],
                    **kwargs
                )
                return response.content[0].text
 
    def monitor_model_performance(self) -> dict:
        """
        Monitor fine-tuned model performance in production.
        Track latency, error rates, and output quality.
        """
 
        return {
            "model_id": self.model_id,
            "requests_processed": 0,
            "avg_latency_ms": 0,
            "error_rate": 0,
            "fallback_rate": 0,
            "last_check": None,
        }

Real-World Example: Financial Analysis Model

Here's a complete example of fine-tuning a model for financial document analysis:

# Step 1: Prepare training data
fintech_preparer = DatasetPreparer()
 
financial_examples = [
    ("EBITDA increased 23% YoY to $4.2B", "positive_growth"),
    ("Operating margin compressed from 18% to 15%", "negative_trend"),
    ("Free cash flow declined due to working capital changes", "concern"),
]
 
for input_text, label in financial_examples:
    fintech_preparer.add_example(
        input_text=f"Classify the financial sentiment: {input_text}",
        output_text=json.dumps({"sentiment": label, "confidence": 0.95}),
        metadata={"type": "sentiment_classification"}
    )
 
# Step 2: Export and validate
export_info = fintech_preparer.export_training_data("fintech_dataset")
 
# Step 3: Train model
tuner = GeminiFineTuner()
job = tuner.create_fine_tuning_job(
    training_file_path="fintech_dataset.train.jsonl",
    eval_file_path="fintech_dataset.eval.jsonl",
    model_id="fintech-sentiment-v1",
    learning_rate=0.0005,
    num_epochs=2,
    batch_size=16,
)
 
# Step 4: Evaluate
evaluator = ModelEvaluator("fintech-sentiment-v1", client)
metrics = evaluator.evaluate_on_dataset("fintech_dataset.eval.jsonl")
 
# Step 5: Deploy
deployment = FineTunedModelDeployment("fintech-sentiment-v1", client)
deployment.prepare_for_production()
 
# Step 6: Use in production
result = deployment.call_with_fallback(
    "Classify: Revenue growth of 15% YoY despite market headwinds"
)

Wrapping up

Fine-tuning Gemini transforms a general-purpose model into a specialist for your domain. The key to success is investing in quality data, carefully choosing evaluation metrics that match your business goals, and thoroughly testing before production.

Start small—fine-tune on a focused task with high-quality data. Once you see consistent improvements, expand to related tasks. And always maintain a fallback strategy to the base model as you shift traffic to your custom versions.

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 $15 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-08-23
Moving app AI work from runtime calls to a pre-ship batch pass
Where you put a Gemini call decides whether your request count scales with users or with assets. Here is the decision rule I used to move classification into a pre-ship batch pass, plus a resumable implementation.
API / SDK2026-08-22
Once You Pass Twenty Mediation Groups, How Do You Find the Setting That Went Missing?
As ad mediation groups multiply, missing sources and type drift accumulate quietly. Here is the split I settled on: normalize the settings into one matrix, let code confirm the gaps, and send Gemini only the cells that need judgment.
API / SDK2026-08-20
Never hand Gemini a raw ANR thread dump
An ANR dump came in at 96 threads and 33,616 characters. Keeping only main cuts 98 percent, but it also deletes the thread that caused the block. Here is a lock-chain preprocessor, measured three ways.
📚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 →