The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
This vignette demonstrates how to use pairwiseLLM for Batch API workflows (server-side batching), which are distinct from the live API calls described in the Getting Started with pairwiseLLM vignette.
Batch workflows are ideal for large-scale jobs because they:
Supported Batch API providers:
run_openai_batch_pipeline())run_anthropic_batch_pipeline())run_gemini_batch_pipeline())Vertex AI Gemini API is intentionally excluded here. In this series,
Vertex is supported on the live path only via
backend = "vertex" with VERTEX_API_KEY, and
generic batch wrappers reject Vertex explicitly.
Recommended approach: For multiple batches (e.g., templates × providers × models × forward/reverse), use:
llm_submit_pairs_multi_batch()to split + submit jobs (no polling; writes an optional registry CSV)llm_resume_multi_batches()to poll + download + parse results (can resume from a registry on disk)These helpers orchestrate the provider-specific pipelines without forcing you to write your own polling loops.
Note: Together.ai and Ollama do not currently support a native Batch API compatible with this workflow. For those providers, use the live API wrapper
submit_llm_pairs()as described in Getting Started with pairwiseLLM.
In this vignette, we will cover:
Note: All heavy API calls in this vignette are set to
eval = FALSEso that the vignette remains CRAN-safe. You can enable them in your own project.
For basic function usage, see the companion vignette:
For prompt evaluation and positional-bias diagnostics, see the companion vignette:
Configure the environment variable only for each provider included in your run plan:
| Provider | Environment Variable |
|---|---|
| OpenAI | OPENAI_API_KEY |
| Anthropic | ANTHROPIC_API_KEY |
| Gemini | GEMINI_API_KEY |
This vignette only covers batch-capable providers. Vertex live
requests use a separate VERTEX_API_KEY surface and are
documented in Getting
Started with pairwiseLLM.
Check which are set:
check_llm_api_keys()
#> All known LLM API keys are set: OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, VERTEX_API_KEY, TOGETHER_API_KEY.
#> # A tibble: 5 × 4
#> backend service env_var has_key
#> <chr> <chr> <chr> <lgl>
#> 1 openai OpenAI OPENAI_API_KEY TRUE
#> 2 anthropic Anthropic ANTHROPIC_API_KEY TRUE
#> 3 gemini Google Gemini GEMINI_API_KEY TRUE
#> 4 vertex Vertex AI Gemini API VERTEX_API_KEY TRUE
#> 5 together Together.ai TOGETHER_API_KEY TRUEWe use the built-in writing samples and a single trait
(overall_quality).
data("example_writing_samples", package = "pairwiseLLM")
td <- trait_description("overall_quality")
td
#> $name
#> [1] "Overall Quality"
#>
#> $description
#> [1] "Overall quality of the writing, considering how well ideas are expressed,\nhow clearly the writing is organized, and how effective the language and\nconventions are."Default prompt template:
tmpl <- set_prompt_template()
cat(substr(tmpl, 1, 400), "...
")
#> You are a debate adjudicator. Your task is to weigh the comparative strengths of two writing samples regarding a specific trait.
#>
#> TRAIT: {TRAIT_NAME}
#> DEFINITION: {TRAIT_DESCRIPTION}
#>
#> SAMPLES:
#>
#> === SAMPLE_1 ===
#> {SAMPLE_1}
#>
#> === SAMPLE_2 ===
#> {SAMPLE_2}
#>
#> EVALUATION PROCESS (Mental Simulation):
#>
#> 1. **Advocate for SAMPLE_1**: Mentally list the single strongest point of evidence that makes SAMPLE_1 the ...Construct a modest number of pairs to keep the example light:
set.seed(123)
pairs_all <- example_writing_samples |>
make_pairs()
n_pairs <- min(40L, nrow(pairs_all))
pairs_forward <- pairs_all |>
sample_pairs(n_pairs = n_pairs, seed = 123) |>
randomize_pair_order(seed = 456)
pairs_reverse <- sample_reverse_pairs(
pairs_forward,
reverse_pct = 1.0,
seed = 789
)
get_pairs_for_direction <- function(direction = c("forward", "reverse")) {
direction <- match.arg(direction)
if (identical(direction, "forward")) {
pairs_forward
} else {
pairs_reverse
}
}Suppose we want to test several prompt templates across:
Here we define a small grid:
anthropic_models <- c(
"claude-haiku-4-5-20251001"
)
gemini_models <- c(
"gemini-3.8-flash"
)
openai_models <- c(
"gpt-4.1",
"gpt-5.6-luna"
)
thinking_levels <- c("no_thinking", "with_thinking")
directions <- c("forward", "reverse")
anthropic_grid <- tidyr::expand_grid(
provider = "anthropic",
model = anthropic_models,
thinking = "no_thinking",
direction = directions
)
gemini_grid <- tidyr::expand_grid(
provider = "gemini",
model = gemini_models,
thinking = "with_thinking",
direction = directions
)
openai_grid <- tidyr::expand_grid(
provider = "openai",
model = openai_models,
thinking = "no_thinking",
direction = directions
)
batch_grid <- dplyr::bind_rows(
anthropic_grid,
gemini_grid,
openai_grid
)
batch_grid
#> # A tibble: 8 × 4
#> provider model thinking direction
#> <chr> <chr> <chr> <chr>
#> 1 anthropic claude-haiku-4-5-20251001 no_thinking forward
#> 2 anthropic claude-haiku-4-5-20251001 no_thinking reverse
#> 3 gemini gemini-3.8-flash with_thinking forward
#> 4 gemini gemini-3.8-flash with_thinking reverse
#> 5 openai gpt-4.1 no_thinking forward
#> 6 openai gpt-4.1 no_thinking reverse
#> 7 openai gpt-5.6-luna no_thinking forward
#> 8 openai gpt-5.6-luna no_thinking reverseWe will also imagine multiple prompt templates have been registered.
For simplicity, we use the same tmpl string, but in
practice you would substitute different text:
templates_tbl <- tibble::tibble(
template_id = c("test1", "test2", "test3", "test4", "test5"),
prompt_template = list(tmpl, tmpl, tmpl, tmpl, tmpl)
)
templates_tbl
#> # A tibble: 5 × 2
#> template_id prompt_template
#> <chr> <list>
#> 1 test1 <chr [1]>
#> 2 test2 <chr [1]>
#> 3 test3 <chr [1]>
#> 4 test4 <chr [1]>
#> 5 test5 <chr [1]>The key idea is:
(template_id, provider, model, thinking, direction) becomes
a runbatch_size or n_segmentsout_root <- "dev-output/advanced-multi-batch"
dir.create(out_root, recursive = TRUE, showWarnings = FALSE)
run_plan <- tidyr::crossing(
templates_tbl |> tidyr::unnest(prompt_template),
batch_grid
) |>
mutate(
run_id = paste(template_id, provider, model, thinking, direction, sep = "__"),
run_id = gsub("[^A-Za-z0-9_.-]+", "-", run_id),
run_dir = file.path(out_root, run_id)
)
run_plan |> dplyr::select(run_id, template_id, provider, model, thinking, direction, run_dir)Below we submit each run using
llm_submit_pairs_multi_batch(). This returns a
jobs list and writes a jobs_registry.csv under
each run directory (because write_registry = TRUE).
Provider-specific options can be forwarded via .... In
the example below we:
submit_one_run <- function(template_id, prompt_template, provider, model, thinking, direction, run_dir) {
pairs_use <- get_pairs_for_direction(direction)
is_thinking <- identical(thinking, "with_thinking")
# Provider-specific knobs (passed through via ...)
extra_args <- list()
if (identical(provider, "openai")) {
# Only request thoughts for models that support them in this workflow
extra_args$include_thoughts <- is_thinking && grepl("^gpt-5", model)
extra_args$include_raw <- TRUE
} else if (identical(provider, "anthropic")) {
extra_args$reasoning <- if (is_thinking) "enabled" else "none"
extra_args$include_thoughts <- is_thinking
extra_args$include_raw <- TRUE
# Optional: set deterministic temperature when not using reasoning
if (!is_thinking) extra_args$temperature <- 0
} else if (identical(provider, "gemini")) {
extra_args$include_thoughts <- TRUE
extra_args$thinking_level <- "low" # example
extra_args$include_raw <- TRUE
}
message(
"Submitting: ", template_id, " | ", provider, " / ", model,
" / ", thinking, " / ", direction
)
# Split strategy:
# - For real jobs, use batch_size (e.g., 500–5000) or n_segments (e.g., 10–50)
# - Here we keep it simple and submit a single segment per run
do.call(
llm_submit_pairs_multi_batch,
c(
list(
pairs = pairs_use,
backend = provider,
model = model,
trait_name = td$name,
trait_description = td$description,
prompt_template = prompt_template,
n_segments = 1L,
output_dir = run_dir,
write_registry = TRUE,
verbose = TRUE
),
extra_args
)
)
}
run_results <- purrr::pmap(
run_plan |>
dplyr::select(
template_id, prompt_template, provider, model, thinking, direction,
run_dir
),
submit_one_run
)
# Store a lightweight manifest so you can resume later without rebuilding run_plan
manifest <- run_plan |>
mutate(registry_path = file.path(run_dir, "jobs_registry.csv"))
manifest_path <- file.path(out_root, "run_manifest.csv")
readr::write_csv(manifest, manifest_path)
manifest_pathAt this point, each run directory contains:
jobs_registry.csv that records all batch IDs and file
paths for that runYou can safely stop R or restart your machine after submission.
To poll all runs, read the manifest and call
llm_resume_multi_batches() for each run_dir.
If you restart R, you can resume without keeping the
jobs objects in memory by setting jobs = NULL
and pointing to output_dir (the function will load
jobs_registry.csv).
manifest_path <- file.path(out_root, "run_manifest.csv")
manifest <- readr::read_csv(manifest_path, show_col_types = FALSE)
poll_one_run <- function(run_dir) {
llm_resume_multi_batches(
jobs = NULL, # load from jobs_registry.csv in run_dir
output_dir = run_dir,
interval_seconds = 60,
per_job_delay = 2,
write_results_csv = TRUE, # writes batch_XX_results.csv files
write_registry = TRUE, # refreshes jobs_registry.csv with done flags
keep_jsonl = TRUE,
verbose = TRUE,
write_combined_csv = TRUE, # writes combined_results.csv inside run_dir
combined_csv_path = "combined_results.csv"
)
}
polled <- purrr::map(manifest$run_dir, poll_one_run)Each element of polled contains a combined
tibble for that run (i.e., all segments bound together). We can attach
run metadata (template/provider/model/thinking/direction) and then bind
all runs into one master table.
combined_all <- purrr::map2_dfr(
polled,
seq_len(nrow(manifest)),
function(res, i) {
meta <- manifest[i, ]
if (is.null(res$combined)) return(NULL)
res$combined |>
mutate(
template_id = meta$template_id,
provider = meta$provider,
model = meta$model,
thinking = meta$thinking,
direction = meta$direction,
run_id = meta$run_id
)
}
)
combined_path <- file.path(out_root, "combined_all_runs.csv")
readr::write_csv(combined_all, combined_path)
combined_pathResuming jobs is possible:
jobs_registry.csv under each run
directoryllm_resume_multi_batches(jobs = NULL, output_dir = <run_dir>)run_manifest.csv with
run_dir paths, resuming all runs is just a
loopExample: resume only unfinished runs (based on each run’s registry):
manifest <- readr::read_csv(file.path(out_root, "run_manifest.csv"), show_col_types = FALSE)
needs_poll <- function(run_dir) {
reg_path <- file.path(run_dir, "jobs_registry.csv")
if (!file.exists(reg_path)) return(FALSE)
reg <- readr::read_csv(reg_path, show_col_types = FALSE)
any(!as.logical(reg$done))
}
unfinished_dirs <- manifest$run_dir[vapply(manifest$run_dir, needs_poll, logical(1))]
polled <- purrr::map(unfinished_dirs, poll_one_run)Once you have per-run results CSVs (e.g., one per template × model × thinking × direction), you can:
compute_reverse_consistency()check_positional_bias()dplyr pipelinesbuild_bt_data() + fit_bt_model()fit_elo_model()
(when EloChoice is installed)Mercer, S. H. (2026). Advanced: Submitting and polling multiple batches [R package vignette]. Comprehensive R Archive Network. https://doi.org/10.32614/CRAN.package.pairwiseLLM
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.