docs/guides/batches.md

# Batches API Guide

The Batches API allows you to submit large numbers of requests at once with 50% cost savings compared to interactive API calls.

## Overview

Batch processing is ideal for:
- Processing large document collections for embeddings
- Bulk content generation for content pipelines
- Overnight processing of accumulated requests
- Cost optimization for high-volume workloads

## Quick Start

```elixir
alias Gemini.APIs.{Files, Batches}
alias Gemini.Types.BatchJob

# 1. Prepare input file (JSONL format)
# Each line: {"contents": [{"parts": [{"text": "..."}]}]}

# 2. Upload the input file
{:ok, input_file} = Files.upload("input.jsonl")

# 3. Create batch job
{:ok, batch} = Batches.create("gemini-2.0-flash",
  file_name: input_file.name,
  display_name: "My Batch Job"
)

# 4. Wait for completion
{:ok, completed} = Batches.wait(batch.name,
  poll_interval: 30_000,
  timeout: 3_600_000
)

# 5. Process results
if BatchJob.succeeded?(completed) do
  IO.puts("Completed #{completed.completion_stats.success_count} requests")
end
```

## Input Format

### JSONL File Format

For file-based batch processing, create a JSONL file where each line is a JSON object:

```json
{"contents": [{"parts": [{"text": "Summarize: First document content here"}]}]}
{"contents": [{"parts": [{"text": "Summarize: Second document content here"}]}]}
{"contents": [{"parts": [{"text": "Summarize: Third document content here"}]}]}
```

### Inlined Requests

For small batches, you can pass requests directly:

```elixir
{:ok, batch} = Batches.create("gemini-2.0-flash",
  inlined_requests: [
    %{contents: [%{parts: [%{text: "Request 1"}]}]},
    %{contents: [%{parts: [%{text: "Request 2"}]}]},
    %{contents: [%{parts: [%{text: "Request 3"}]}]}
  ],
  display_name: "Small Batch"
)
```

## Creating Batch Jobs

### Content Generation Batch

```elixir
{:ok, batch} = Batches.create("gemini-2.0-flash",
  file_name: "files/input123",
  display_name: "Content Generation Batch",
  generation_config: %{
    temperature: 0.7,
    maxOutputTokens: 1000
  }
)
```

### Embedding Batch

```elixir
{:ok, batch} = Batches.create_embeddings("text-embedding-004",
  file_name: "files/embeddings-input",
  display_name: "Embedding Batch"
)
```

### With System Instruction

```elixir
{:ok, batch} = Batches.create("gemini-2.0-flash",
  file_name: "files/input123",
  system_instruction: %{
    parts: [%{text: "You are a helpful assistant that summarizes documents concisely."}]
  }
)
```

## Batch Job States

| State | Description |
|-------|-------------|
| `:queued` | Job is queued for processing |
| `:pending` | Job is preparing to run |
| `:running` | Job is actively processing |
| `:succeeded` | Job completed successfully |
| `:failed` | Job failed |
| `:cancelling` | Job is being cancelled |
| `:cancelled` | Job was cancelled |
| `:expired` | Job expired |
| `:partially_succeeded` | Some requests succeeded, some failed |

## Monitoring Batch Jobs

### Get Job Status

```elixir
{:ok, batch} = Batches.get("batches/abc123")

IO.puts("State: #{batch.state}")

if batch.completion_stats do
  stats = batch.completion_stats
  IO.puts("Total: #{stats.total_count}")
  IO.puts("Success: #{stats.success_count}")
  IO.puts("Failed: #{stats.failure_count}")
end
```

### Wait for Completion

```elixir
{:ok, completed} = Batches.wait("batches/abc123",
  poll_interval: 60_000,   # Check every minute
  timeout: 7_200_000,      # Wait up to 2 hours
  on_progress: fn batch ->
    if progress = BatchJob.get_progress(batch) do
      IO.puts("Progress: #{Float.round(progress, 1)}%")
    end
  end
)

cond do
  BatchJob.succeeded?(completed) ->
    IO.puts("Success!")

  BatchJob.failed?(completed) ->
    IO.puts("Failed: #{completed.error.message}")

  BatchJob.cancelled?(completed) ->
    IO.puts("Job was cancelled")
end
```

## Listing Batch Jobs

### List with Pagination

```elixir
{:ok, response} = Batches.list()

Enum.each(response.batch_jobs, fn job ->
  IO.puts("#{job.name}: #{job.state}")
end)

# With pagination
{:ok, response} = Batches.list(page_size: 10)
if ListBatchJobsResponse.has_more_pages?(response) do
  {:ok, page2} = Batches.list(page_token: response.next_page_token)
end
```

### List All Jobs

```elixir
{:ok, all_jobs} = Batches.list_all()
running = Enum.filter(all_jobs, &BatchJob.running?/1)
IO.puts("Running jobs: #{length(running)}")
```

## Cancelling and Deleting

### Cancel a Running Job

```elixir
:ok = Batches.cancel("batches/abc123")
```

### Delete a Completed Job

```elixir
:ok = Batches.delete("batches/abc123")
```

## Getting Results

### Inlined Responses

For batches with inline response output:

```elixir
{:ok, batch} = Batches.get("batches/abc123")

if BatchJob.succeeded?(batch) do
  case Batches.get_responses(batch) do
    {:ok, responses} ->
      Enum.each(responses, fn response ->
        IO.inspect(response)
      end)

    {:error, {:file_output, file_name}} ->
      IO.puts("Results in file: #{file_name}")

    {:error, {:gcs_output, gcs_uri}} ->
      IO.puts("Results in GCS: #{gcs_uri}")
  end
end
```

## Batch Job Helper Functions

```elixir
alias Gemini.Types.BatchJob

# Check job state
BatchJob.complete?(batch)     # Terminal state?
BatchJob.succeeded?(batch)    # Completed successfully?
BatchJob.failed?(batch)       # Failed?
BatchJob.running?(batch)      # Still processing?
BatchJob.cancelled?(batch)    # Was cancelled?

# Get progress percentage
BatchJob.get_progress(batch)  # 75.5 or nil

# Get job ID
BatchJob.get_id(batch)        # "abc123" from "batches/abc123"
```

## GCS and BigQuery Integration

### GCS Source (Vertex AI)

```elixir
{:ok, batch} = Batches.create("gemini-2.0-flash",
  gcs_uri: ["gs://my-bucket/input.jsonl"],
  auth: :vertex_ai
)
```

### BigQuery Source (Vertex AI)

```elixir
{:ok, batch} = Batches.create("gemini-2.0-flash",
  bigquery_uri: "bq://project.dataset.table",
  auth: :vertex_ai
)
```

## Error Handling

```elixir
case Batches.create("gemini-2.0-flash", file_name: "files/input") do
  {:ok, batch} ->
    IO.puts("Created: #{batch.name}")

  {:error, {:http_error, 400, body}} ->
    IO.puts("Invalid request: #{inspect(body)}")

  {:error, {:http_error, 429, _}} ->
    IO.puts("Rate limited, try again later")

  {:error, reason} ->
    IO.puts("Failed: #{inspect(reason)}")
end
```

## Best Practices

1. **Use files for large batches** - Inlined requests are limited in size
2. **Monitor progress** - Use the `on_progress` callback for long-running jobs
3. **Handle failures gracefully** - Check `completion_stats` for partial failures
4. **Clean up completed jobs** - Delete jobs when results are processed
5. **Set appropriate timeouts** - Batch jobs can take hours for large inputs

## Cost Savings

Batch processing offers 50% cost savings compared to interactive API calls:
- Interactive: Full price per request
- Batch: 50% discount, processed during off-peak times

## API Reference

- `Gemini.APIs.Batches.create/2` - Create content generation batch
- `Gemini.APIs.Batches.create_embeddings/2` - Create embedding batch
- `Gemini.APIs.Batches.get/2` - Get batch job status
- `Gemini.APIs.Batches.list/1` - List batch jobs
- `Gemini.APIs.Batches.list_all/1` - List all batch jobs
- `Gemini.APIs.Batches.cancel/2` - Cancel a running batch
- `Gemini.APIs.Batches.delete/2` - Delete a batch job
- `Gemini.APIs.Batches.wait/2` - Wait for batch completion
- `Gemini.APIs.Batches.get_responses/1` - Get inlined responses