localarena 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to LocalArena are documented here.
4
4
 
5
5
  The project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.2.0] - 2026-07-26
8
+
9
+ ### Added
10
+
11
+ - Live OpenAI-compatible providers for llama.cpp, Ollama, LM Studio,
12
+ OpenRouter, OpenAI, and custom base URLs.
13
+ - Concurrent model-by-task evaluation with exact, containment, regular
14
+ expression, JSON, numeric, and live model-judge scoring.
15
+ - A Python CLI for provider discovery, live runs, saved results, and
16
+ standalone HTML reports.
17
+ - Shared evaluation configuration and run schemas, privacy-safe result
18
+ serialization, and judge-only model configuration.
19
+ - Matching JavaScript APIs, TypeScript declarations, normalized usage data,
20
+ bounded transport behavior, and cancellation.
21
+
22
+ ### Changed
23
+
24
+ - Prompt, answer, evaluator, and diagnostic content is excluded from saved
25
+ results by default and requires an explicit opt-in.
26
+ - Generation retries are disabled by default to avoid duplicating ambiguous
27
+ provider requests or billing.
28
+
7
29
  ## [0.1.0] - 2026-07-25
8
30
 
9
31
  ### Added
package/README.md CHANGED
@@ -1,210 +1,322 @@
1
- # localarena
1
+ # LocalArena
2
2
 
3
- Local-first pairwise evaluation, deterministic scheduling, and Elo ranking for
4
- models, agents, prompts, algorithms, or anything else you want to compare.
3
+ Run the models you choose against the tasks you choose, using live provider
4
+ responses from your machine or an explicitly configured API endpoint.
5
5
 
6
6
  [![PyPI](https://img.shields.io/pypi/v/localarena)](https://pypi.org/project/localarena/)
7
7
  [![npm](https://img.shields.io/npm/v/localarena)](https://www.npmjs.com/package/localarena)
8
8
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
9
9
 
10
- `localarena` is the small, dependency-free engine underneath a head-to-head
11
- arena. You generate two outputs and decide which side won; LocalArena keeps the
12
- match log, updates ratings, builds a fair next pairing, and gives you a portable
13
- leaderboard.
10
+ LocalArena executes the complete model × task grid concurrently, applies
11
+ deterministic scorers or a live judge model, records failures as data, derives
12
+ Elo standings, and writes a standalone HTML report. It never substitutes
13
+ recorded answers or copied leaderboard results for a live call.
14
14
 
15
- - Same package name and concepts in Python and JavaScript
16
- - No network calls, telemetry, model SDK, database, or implicit file writes
17
- - Deterministic round-robin and least-played scheduling
18
- - Standard Elo updates with wins, losses, and draws
19
- - Versioned JSON snapshots that can move between both runtimes
20
- - Detached metadata so callers cannot accidentally mutate arena state
15
+ - llama.cpp, Ollama, and LM Studio on loopback or any reachable host
16
+ - OpenRouter and OpenAI with separate runtime credentials
17
+ - Any compatible Chat Completions endpoint through a custom base URL
18
+ - Python CLI and library, plus matching JavaScript and TypeScript APIs
19
+ - Exact, contains, regular-expression, JSON, numeric, and model-judge scoring
20
+ - Privacy-safe result files by default: prompts and answers require opt-in
21
+ - No runtime dependencies in either package
21
22
 
22
23
  ## Install
23
24
 
24
- Python:
25
+ Python 3.10+:
25
26
 
26
27
  ```bash
27
28
  pip install localarena
28
29
  ```
29
30
 
30
- JavaScript or TypeScript (Node.js 18+):
31
+ Node.js 18+:
31
32
 
32
33
  ```bash
33
34
  npm install localarena
34
35
  ```
35
36
 
36
- ## Quick start
37
+ ## Run a live evaluation
37
38
 
38
- ### Python
39
+ Create `evaluation.json`:
39
40
 
40
- ```python
41
- from localarena import Arena, Result
41
+ ```json
42
+ {
43
+ "$schema": "./node_modules/localarena/schema/localarena-evaluation-config-v1.schema.json",
44
+ "name": "Local smoke test",
45
+ "concurrency": 2,
46
+ "models": [
47
+ {
48
+ "name": "local-model",
49
+ "provider": "ollama",
50
+ "model": "qwen3:0.6b",
51
+ "parameters": {
52
+ "max_tokens": 64,
53
+ "temperature": 0
54
+ }
55
+ }
56
+ ],
57
+ "tasks": [
58
+ {
59
+ "id": "arithmetic",
60
+ "prompt": "Return only the result of 6 multiplied by 7.",
61
+ "evaluator": {
62
+ "type": "numeric",
63
+ "expected": 42,
64
+ "tolerance": 0
65
+ }
66
+ },
67
+ {
68
+ "id": "structured",
69
+ "prompt": "Return only JSON with an answer field equal to 42.",
70
+ "evaluator": {
71
+ "type": "json",
72
+ "compare": true,
73
+ "expected": {
74
+ "answer": 42
75
+ }
76
+ }
77
+ }
78
+ ]
79
+ }
80
+ ```
42
81
 
43
- arena = Arena(["ollama/qwen3", "lm-studio/gemma"])
82
+ Run every configured model against every configured task:
44
83
 
45
- # Generate both answers locally, ask a human or judge, then record the verdict.
46
- arena.record(
47
- "ollama/qwen3",
48
- "lm-studio/gemma",
49
- Result.LEFT,
50
- metadata={"prompt_id": "explain-rag"},
51
- )
84
+ ```bash
85
+ localarena run evaluation.json \
86
+ --output results.json \
87
+ --report report.html
88
+ ```
52
89
 
53
- for standing in arena.standings():
54
- print(standing.name, round(standing.rating, 2))
90
+ The command prints progress as calls complete and returns a nonzero status if
91
+ any generation or scoring row failed. A failure stays in `results.json`; it
92
+ does not abort or silently shrink the matrix.
55
93
 
56
- print("Compare next:", arena.next_pair())
57
- ```
94
+ The complete multi-provider example is
95
+ [examples/evaluation-config.json](examples/evaluation-config.json).
58
96
 
59
- ### JavaScript
97
+ ## Providers
60
98
 
61
- ```js
62
- import { Arena, Result } from "localarena";
99
+ All built-in profiles use bounded, non-streaming Chat Completions requests and
100
+ model discovery through `/v1/models`.
63
101
 
64
- const arena = new Arena(["ollama/qwen3", "lm-studio/gemma"]);
102
+ | Profile | Default base URL | Default credential |
103
+ | --- | --- | --- |
104
+ | `llamacpp` | `http://127.0.0.1:8080/v1` | None |
105
+ | `ollama` | `http://127.0.0.1:11434/v1` | None |
106
+ | `lmstudio` | `http://127.0.0.1:1234/v1` | None |
107
+ | `openrouter` | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` |
108
+ | `openai` | `https://api.openai.com/v1` | `OPENAI_API_KEY` |
109
+ | `custom` | Required in config | Optional `api_key_env` |
65
110
 
66
- arena.record("ollama/qwen3", "lm-studio/gemma", Result.LEFT, {
67
- prompt_id: "explain-rag",
68
- });
111
+ A model entry may override `base_url`, select any model ID, set generation
112
+ parameters, and read credentials or sensitive headers from environment
113
+ variables. Literal API keys are rejected by the CLI configuration loader.
114
+ Hosted-provider keys are isolated: a standard cloud key is never forwarded
115
+ automatically when that profile is pointed at a different base URL.
69
116
 
70
- for (const standing of arena.standings()) {
71
- console.log(standing.name, standing.rating.toFixed(2));
72
- }
117
+ Useful commands:
73
118
 
74
- console.log("Compare next:", arena.nextPair());
119
+ ```bash
120
+ localarena providers
121
+ localarena models ollama
122
+ localarena models custom --base-url http://127.0.0.1:9000/v1
123
+ localarena report results.json --output report.html
124
+ ```
125
+
126
+ For an authenticated custom endpoint:
127
+
128
+ ```bash
129
+ export LOCALARENA_CUSTOM_API_KEY="your-key"
130
+ localarena models custom \
131
+ --base-url https://inference.example.com/v1 \
132
+ --api-key-env LOCALARENA_CUSTOM_API_KEY
75
133
  ```
76
134
 
77
- The library intentionally does not decide how outputs are generated or judged.
78
- That keeps private prompts and responses on your machine and lets the same
79
- engine work with Ollama, LM Studio, vLLM, llama.cpp, local functions, human
80
- votes, or any provider SDK you explicitly choose.
135
+ Requests make one attempt by default because replaying an ambiguous generation
136
+ can duplicate work or billing. Retries are explicit through
137
+ `policy.max_attempts`.
138
+
139
+ ## Tasks and scoring
140
+
141
+ Every native task is provider-independent JSON-safe chat messages plus one
142
+ optional evaluator:
143
+
144
+ | Evaluator | Use |
145
+ | --- | --- |
146
+ | `exact` | Complete answer equality with explicit whitespace/case rules |
147
+ | `contains` | Require all or any configured strings |
148
+ | `regex` | Search or full-output format validation |
149
+ | `json` | Strict JSON validation or type-sensitive JSON comparison |
150
+ | `numeric` | Numeric answer with an absolute tolerance |
151
+ | `model_judge` | Live rubric scoring from another configured model |
152
+ | `null` | Generate and retain run metadata without a score |
153
+
154
+ Set `"judge_only": true` on a model entry to use that model for
155
+ `model_judge` tasks without evaluating or ranking it as a contestant.
156
+
157
+ `concurrency` bounds the number of in-flight rows. `repetitions` repeats the
158
+ entire grid. Each model keeps its own base URL, credential, model ID, and
159
+ generation settings, so local and hosted targets can run in the same
160
+ evaluation.
161
+
162
+ ## Results and reports
163
+
164
+ `results.json` follows the versioned
165
+ [evaluation run schema](schema/localarena-evaluation-run-v1.schema.json). It
166
+ contains:
167
+
168
+ - one record for every model, task, and repetition;
169
+ - normalized status, timing, finish reason, token use, and attempt count;
170
+ - deterministic or live-judge scores;
171
+ - per-model score, pass rate, error, latency, token, and judge aggregates;
172
+ - Elo standings derived only from comparable successful task scores; and
173
+ - an arena snapshot whose match history can move between Python and
174
+ JavaScript.
175
+
176
+ JSON Schema validates the portable document shape. Load untrusted snapshots
177
+ with `run_from_dict` or `runFromSnapshot` as well; the SDK loaders enforce the
178
+ cross-field guarantee that every configured model × task × repetition has
179
+ exactly one row.
180
+
181
+ The HTML report is a single local file with no scripts, fonts, stylesheets, or
182
+ network requests. It includes a leaderboard, score bars, a model-by-task heat
183
+ map, and expandable row diagnostics.
184
+
185
+ Prompt text, evaluator criteria and reference answers, generated text, score
186
+ reasons, task metadata, and detailed errors are excluded from saved results
187
+ and reports by default. Non-secret evaluator type and live-judge target
188
+ provenance remain visible so scoring failures are attributable. Opt in only
189
+ when the content is safe to retain:
190
+
191
+ ```bash
192
+ localarena run evaluation.json \
193
+ --include-content \
194
+ --output results-with-content.json \
195
+ --report report-with-content.html
196
+ ```
81
197
 
82
- ## Schedule a full arena
198
+ Credentials, authorization headers, base URLs, and configured headers are
199
+ never serialized into an evaluation run.
83
200
 
84
- `round_robin` / `roundRobin` returns each unordered pair once per round.
85
- Pair orientation flips every second round to reduce persistent left/right bias.
201
+ ## Python API
86
202
 
87
203
  ```python
88
- from localarena import Arena, Result, round_robin
204
+ from localarena import (
205
+ EvaluationRunner,
206
+ ExactMatch,
207
+ ModelTarget,
208
+ PromptTask,
209
+ create_provider,
210
+ write_html_report,
211
+ )
89
212
 
90
- models = ["a", "b", "c"]
91
- arena = Arena(models)
213
+ provider = create_provider("ollama")
214
+ target = ModelTarget(
215
+ name="local-model",
216
+ provider=provider,
217
+ model="qwen3:0.6b",
218
+ max_tokens=64,
219
+ temperature=0,
220
+ )
221
+ task = PromptTask.from_text(
222
+ "capital",
223
+ "Reply with exactly Paris.",
224
+ evaluator=ExactMatch("Paris"),
225
+ )
92
226
 
93
- for left, right in round_robin(models, rounds=2):
94
- # Replace this deterministic example with your own comparison.
95
- winner = Result.LEFT if len(left) >= len(right) else Result.RIGHT
96
- arena.record(left, right, winner)
227
+ run = EvaluationRunner([target], [task], max_concurrency=2).run(
228
+ name="Python API example"
229
+ )
230
+ print(run.to_json())
231
+ write_html_report(run, "report.html")
97
232
  ```
98
233
 
99
- ```js
100
- import { Arena, Result, roundRobin } from "localarena";
234
+ Provider construction does not perform network I/O. Calls occur only through
235
+ `list_models()`, `generate()`, or an evaluation runner.
101
236
 
102
- const models = ["a", "b", "c"];
103
- const arena = new Arena(models);
237
+ ## JavaScript API
104
238
 
105
- for (const [left, right] of roundRobin(models, 2)) {
106
- const winner = left.length >= right.length ? Result.LEFT : Result.RIGHT;
107
- arena.record(left, right, winner);
108
- }
239
+ ```js
240
+ import {
241
+ EvaluationRunner,
242
+ ExactMatch,
243
+ ModelTarget,
244
+ PromptTask,
245
+ createProvider,
246
+ writeHTMLReport,
247
+ } from "localarena";
248
+
249
+ const target = new ModelTarget({
250
+ name: "local-model",
251
+ provider: createProvider("ollama"),
252
+ model: "qwen3:0.6b",
253
+ requestOverrides: { maxTokens: 64, temperature: 0 },
254
+ });
255
+ const task = PromptTask.fromText(
256
+ "capital",
257
+ "Reply with exactly Paris.",
258
+ { evaluator: new ExactMatch("Paris") },
259
+ );
260
+
261
+ const run = await new EvaluationRunner([target], [task], {
262
+ maxConcurrency: 2,
263
+ }).run({ name: "JavaScript API example" });
264
+
265
+ console.log(run.toJSONString());
266
+ await writeHTMLReport(run, "report.html");
109
267
  ```
110
268
 
111
- ## Portable snapshots
269
+ `AbortSignal` is supported for JavaScript provider calls and complete
270
+ evaluation runs.
112
271
 
113
- Standings are derived from an append-only match history. A snapshot contains
114
- only the arena settings, contestant metadata, and that history:
272
+ Python's `arun()` stops scheduling new rows when its coroutine is cancelled.
273
+ A synchronous provider request already running in a worker thread continues
274
+ only until that provider call completes or reaches its configured timeout.
115
275
 
116
- ```python
117
- import json
118
- from localarena import Arena
276
+ ## Terminal and container benchmarks
119
277
 
120
- payload = json.dumps(arena.snapshot(), ensure_ascii=False, indent=2)
121
- restored = Arena.from_json(payload)
122
- ```
278
+ A prompt-only completion is not an official terminal benchmark run. Tasks that
279
+ depend on a repository, shell, tools, container image, mutable state, time
280
+ limits, or a benchmark-specific grader must stay in their authoritative
281
+ harness. An adapter can import that harness result and reproducibility metadata
282
+ into the rating layer; LocalArena does not pretend that sending the task prompt
283
+ to a chat endpoint reproduces the environment.
123
284
 
124
- ```js
125
- const payload = JSON.stringify(arena, null, 2);
126
- const restored = Arena.fromJSON(payload);
127
- ```
285
+ See [provider and task support](docs/provider-and-task-support.md) for the
286
+ protocol rationale and external-adapter boundary.
128
287
 
129
- The schema-v1 keys are deliberately shared:
288
+ ## Elo core
130
289
 
131
- ```json
132
- {
133
- "schema_version": 1,
134
- "initial_rating": 1000,
135
- "k_factor": 32,
136
- "contestants": [
137
- {"name": "a", "metadata": {}},
138
- {"name": "b", "metadata": {}}
139
- ],
140
- "matches": [
141
- {
142
- "id": 1,
143
- "left": "a",
144
- "right": "b",
145
- "result": "left",
146
- "metadata": {}
147
- }
148
- ]
149
- }
150
- ```
151
-
152
- See [the JSON Schema](schema/localarena-v1.schema.json) for the serialized
153
- contract. Restoring a snapshot replays the history instead of trusting stored
154
- ratings or counters.
290
+ The original dependency-free arena remains available for human or external
291
+ verdicts:
155
292
 
156
- ## Core API
293
+ ```python
294
+ from localarena import Arena, Result
157
295
 
158
- | Python | JavaScript | Purpose |
159
- | --- | --- | --- |
160
- | `Arena(names, initial_rating=..., k_factor=...)` | `new Arena(names, { initialRating, kFactor })` | Create an arena |
161
- | `add(name, metadata)` | `add(name, metadata)` | Register a contestant |
162
- | `rating(name)` | `rating(name)` | Read one current rating |
163
- | `record(left, right, result, metadata)` | `record(left, right, result, metadata)` | Record one match |
164
- | `standings()` / `leaderboard()` | `standings()` / `leaderboard()` | Return rating-sorted rows |
165
- | `next_pair()` | `nextPair()` | Choose the least-played pair |
166
- | `snapshot()` / `to_json()` | `snapshot()` / `JSON.stringify(arena)` | Export schema v1 |
167
- | `Arena.from_snapshot()` / `from_json()` | `Arena.fromSnapshot()` / `fromJSON()` | Validate and replay |
168
- | `expected_score(a, b)` | `expectedScore(a, b)` | Compute Elo expectation |
169
- | `round_robin(names, rounds)` | `roundRobin(names, rounds)` | Build all-pairs schedule |
170
-
171
- Results are `"left"`, `"right"`, or `"draw"`. Python also exports the
172
- `Result` string enum; JavaScript exports a frozen `Result` object.
173
-
174
- ## Rating behavior
175
-
176
- LocalArena uses the conventional Elo expectation:
177
-
178
- ```text
179
- E(left) = 1 / (1 + 10 ^ ((right_rating - left_rating) / 400))
296
+ arena = Arena(["model-a", "model-b"])
297
+ arena.record("model-a", "model-b", Result.LEFT)
298
+ print(arena.standings())
180
299
  ```
181
300
 
182
- A win scores `1`, a draw `0.5`, and a loss `0`. Both contestants receive equal
183
- and opposite rating adjustments, so total rating is conserved apart from
184
- normal floating-point precision.
185
-
186
- Elo is an online rating system: changing match order can change the final
187
- ratings. Keep and share the match log when reproducibility matters.
301
+ The JavaScript package exposes the same `Arena`, `Result`, `roundRobin`, and
302
+ schema-v1 snapshot contract.
188
303
 
189
- ## Development
304
+ ## Development and release
190
305
 
191
306
  ```bash
192
- # Python
193
307
  python3 -m pip install -e python
194
308
  python3 -m unittest discover -s python/tests
195
- python3 -m build python
196
- python3 -m twine check python/dist/*
309
+ python3 scripts/check_runtime_parity.py
197
310
 
198
- # JavaScript
199
311
  npm ci
312
+ npm run check
200
313
  npm test
201
- npm audit --audit-level=low
202
314
  npm pack --dry-run
203
315
  ```
204
316
 
205
- CI tests all supported runtimes. Pushing an immutable `v*` tag runs the release
206
- workflow, verifies that the tag, Python package, and npm package versions agree,
207
- then publishes the exact tested artifacts to PyPI and npm.
317
+ CI covers Python 3.10–3.13 and Node.js 18, 20, 22, and 24. A matching immutable
318
+ `v*` tag builds and verifies both distributions, then publishes the exact
319
+ artifacts to PyPI and npm.
208
320
 
209
321
  ## License
210
322
 
@@ -0,0 +1,165 @@
1
+ # Provider and task support
2
+
3
+ LocalArena evaluates outputs produced during the current run. It does not ship
4
+ recorded model answers, copy scores from public leaderboards, or substitute
5
+ cached responses when an endpoint is unavailable.
6
+
7
+ The rating engine remains offline and dependency-free. Provider calls are an
8
+ optional execution layer around that engine: generate two live answers, obtain
9
+ a verdict, and record `left`, `right`, or `draw` with provenance in match
10
+ metadata.
11
+
12
+ ## Provider architecture
13
+
14
+ The first provider set uses one OpenAI-compatible Chat Completions transport
15
+ with five named presets. A preset supplies safe defaults such as the base URL
16
+ and expected authentication; the request, response, error, and model-listing
17
+ logic stays shared.
18
+
19
+ | Provider | Location | Default base URL | Chat and model endpoints | Authentication | Release behavior and notes | LocalArena adapter |
20
+ | --- | --- | --- | --- | --- | --- | --- |
21
+ | [llama.cpp server](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md) | Local or self-hosted | `http://127.0.0.1:8080/v1` | `POST /chat/completions`; `GET /models` | None by default; Bearer token when the server is started with `--api-key` | Bounded non-streaming requests; use a server alias for a stable model ID, and configure a compatible chat template | Named preset over the shared transport |
22
+ | [Ollama](https://docs.ollama.com/api/openai-compatibility) | Local | `http://127.0.0.1:11434/v1` | `POST /chat/completions`; `GET /models` | No server credential by default; some clients require a non-secret placeholder value, which Ollama ignores | Bounded non-streaming requests; models must be pulled before use | Named preset over the shared transport |
23
+ | [LM Studio](https://lmstudio.ai/docs/developer/openai-compat) | Local | `http://127.0.0.1:1234/v1` | `POST /chat/completions`; `GET /models` | None when authentication is disabled; Bearer token when [authentication is enabled](https://lmstudio.ai/docs/developer/core/authentication) | Bounded non-streaming requests; the selected model must be available to the local server | Named preset over the shared transport |
24
+ | [OpenRouter](https://openrouter.ai/docs/quickstart) | Hosted | `https://openrouter.ai/api/v1` | `POST /chat/completions`; [`GET /models`](https://openrouter.ai/docs/guides/overview/models) | Bearer API key | Bounded non-streaming requests; optional `HTTP-Referer` and `X-OpenRouter-Title` attribution headers | Named preset over the shared transport |
25
+ | [OpenAI](https://developers.openai.com/api/reference/resources/chat) | Hosted | `https://api.openai.com/v1` | `POST /chat/completions`; [`GET /models`](https://developers.openai.com/api/reference/resources/models) | Bearer API key; optional organization and project headers | Bounded non-streaming requests | Reference preset for the shared transport |
26
+
27
+ For all five presets, this release sends `stream: false`, reads the answer from
28
+ `choices[0].message.content`, and reads model IDs from `data[].id`. Responses
29
+ are buffered only up to the configured byte limit. The transport accepts an
30
+ explicit base URL, so another compatible local or hosted server can be used
31
+ without adding a new protocol implementation.
32
+
33
+ ### Why one transport
34
+
35
+ All five servers deliberately expose the same small interoperability surface:
36
+ Chat Completions, model listing, Bearer-style headers when authentication is
37
+ needed, and the same core response envelope. Sharing that transport provides:
38
+
39
+ - the same core request fields and normalized result contract in the Python
40
+ and JavaScript packages;
41
+ - one place for timeouts, bounded response parsing, retries, response
42
+ validation, error normalization, and secret redaction;
43
+ - contract tests that apply to every preset;
44
+ - custom-base-URL support without a provider-specific dependency; and
45
+ - a small portable parameter set: `model`, `messages`, `max_tokens`,
46
+ `temperature`, `seed`, and stop sequences.
47
+
48
+ A compatibility layer is not a claim that every provider supports every
49
+ vendor-specific feature. Provider-only request fields remain explicit
50
+ pass-through options, and callers should use only fields supported by their
51
+ selected endpoint. A genuinely different wire protocol belongs in a separate
52
+ transport rather than in conditionals scattered through the shared one.
53
+
54
+ ## Live execution and network guarantees
55
+
56
+ Provider-backed evaluation follows an explicit-network contract:
57
+
58
+ 1. Importing LocalArena, constructing an arena or provider, scheduling pairs,
59
+ reading standings, and loading or saving snapshots perform no network I/O.
60
+ 2. A network request occurs only when the caller explicitly asks to list
61
+ models, generate an answer, or run a provider-backed task.
62
+ 3. Each request goes only to the configured base URL. There is no silent
63
+ provider fallback, cloud fallback, endpoint discovery, model download, or
64
+ model pull.
65
+ 4. Local presets default to loopback addresses. Hosted presets must be chosen
66
+ explicitly and require their own credential.
67
+ 5. Generation is live. An unreachable endpoint, missing model, invalid
68
+ response, or timeout is reported as an error; it is never replaced with a
69
+ bundled or previously recorded answer.
70
+ 6. Generation makes one attempt by default. Retries are opt-in; when enabled,
71
+ they repeat the same configured request and remain visible in run metadata.
72
+ They do not switch models or providers.
73
+
74
+ Custom HTTP base URLs are useful for a trusted machine on a private network.
75
+ Hosted credentials should be sent only over HTTPS. The caller remains
76
+ responsible for trusting a custom endpoint and for deciding whether prompts may
77
+ leave the local machine.
78
+
79
+ ## Secret handling
80
+
81
+ API keys are runtime configuration, not arena data.
82
+
83
+ - Keys may be supplied directly for the current process or read from a
84
+ provider-specific environment variable.
85
+ - A key is placed only in the authentication header for the configured
86
+ endpoint. It is not added to a URL, prompt, response, match metadata, or
87
+ snapshot.
88
+ - Request diagnostics and exceptions redact authentication and caller-supplied
89
+ secret headers.
90
+ - Result content retention defaults off. Prompts, evaluator criteria and
91
+ references, answer text, score reasons, and error details are retained only
92
+ when the caller explicitly opts in. Non-secret evaluator type and judge
93
+ target provenance remain available for auditability.
94
+ - Unknown custom exceptions are recorded with generic details so an endpoint
95
+ or credential embedded in an exception cannot leak into a result. Structured
96
+ provider failures may add sanitized status, retryability, and attempt counts.
97
+ - Local endpoints work without a real credential unless their operator has
98
+ enabled authentication.
99
+ - OpenAI and OpenRouter use separate credentials. A missing key fails before a
100
+ hosted request is sent.
101
+ - Snapshots can contain sanitized provenance such as provider name, model ID,
102
+ latency, token usage, finish reason, and a non-secret endpoint label. They
103
+ must never contain authorization headers or raw keys.
104
+
105
+ ## Task taxonomy
106
+
107
+ The execution boundary is determined by what is needed to run and grade a task,
108
+ not by the benchmark's name.
109
+
110
+ | Task class | Examples | Execution path | Result recorded by LocalArena |
111
+ | --- | --- | --- | --- |
112
+ | Prompt preference | Two answers judged by a person or a live judge model | Native | `left`, `right`, or `draw`, plus prompt and run provenance |
113
+ | Prompt with a deterministic scorer | Exact or normalized match, structured-output validation, classification, extraction, or caller-supplied scoring code | Native | The scorer's comparison converted to `left`, `right`, or `draw` |
114
+ | Prompt with a rubric or reference | Free-form answer evaluated live against criteria or a reference answer | Native | The live evaluator's verdict and sanitized evidence |
115
+ | Stateful tool or simulator task | A task that needs browser state, an external simulator, a service topology, or another controlled runtime | External adapter | A normalized verdict imported from the task's authoritative runner |
116
+ | Containerized terminal benchmark | Repository repair, shell interaction, tests, filesystem mutation, environment reset, or image-based grading | Official environment and agent harness through an external adapter | The official harness outcome, task ID, and reproducibility metadata |
117
+ | Historical or third-party result | A result created outside the current run | Explicit import only | Marked as imported; never presented as a live LocalArena execution |
118
+
119
+ ### Native prompt-scored tasks
120
+
121
+ A task runs natively when it can be represented by JSON-safe messages and its
122
+ score can be derived from returned content plus task-local criteria. LocalArena
123
+ can schedule the pair, call each configured provider, invoke the scorer or live
124
+ judge, and record the outcome without taking control of a shell or mutable
125
+ environment. A target marked `judge_only` can score other targets without
126
+ appearing as a contestant or receiving an arena rating.
127
+
128
+ Native task runs should preserve enough provenance to reproduce the comparison:
129
+ task and prompt identifiers or hashes, provider and model IDs, sampling
130
+ parameters, judge configuration, timestamps, latency, token usage when
131
+ reported, finish reason, and the scorer version. Secrets and sensitive prompt
132
+ content remain opt-in metadata and are excluded by default.
133
+
134
+ ### External terminal and container tasks
135
+
136
+ A terminal benchmark is more than a prompt. Its result depends on the exact
137
+ repository state, container image, installed tools, shell policy, time limits,
138
+ network policy, environment reset, agent loop, and grader. Reimplementing only
139
+ its prompt through a chat endpoint would not reproduce the benchmark and must
140
+ not be described as an official run.
141
+
142
+ These benchmarks therefore stay under their published environment and agent
143
+ harness. A narrow external adapter starts or observes that harness and returns
144
+ a normalized run record to LocalArena. At minimum, that record should include:
145
+
146
+ - benchmark and harness version;
147
+ - task ID and attempt ID;
148
+ - contestant and model configuration;
149
+ - container image digest or equivalent environment identifier;
150
+ - seed, start and finish timestamps, status, and timeout state;
151
+ - authoritative score or pass/fail outcome;
152
+ - references to logs or grader evidence; and
153
+ - the mapping from that outcome to `left`, `right`, or `draw`.
154
+
155
+ The adapter boundary keeps container lifecycle, tool execution, credentials,
156
+ and benchmark-specific grading outside the dependency-free rating core. It also
157
+ allows an official harness to evolve independently while LocalArena continues
158
+ to schedule comparisons, retain provenance, and calculate standings.
159
+
160
+ ## Support rule
161
+
162
+ Use the native path when the full task is “messages in, content out, score the
163
+ content.” Use an external adapter when correctness depends on state or tools
164
+ outside that exchange. In both paths, results come from an execution requested
165
+ by the caller; no precomputed leaderboard data is treated as a live run.