@wendellresearch-ai/benchwarmers 0.2.2 → 0.3.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/GATEWAY.md CHANGED
@@ -205,7 +205,7 @@ request becomes OpenAI-style messages, the panel runs exactly as on
205
205
  scope, rate caps, CORS, the call log), and the reply goes back in the
206
206
  format's own shape. The model field is the collection: `bench/core`, or
207
207
  `bench/core:merged` for one merged answer. Clients that pick from a list
208
- (`/api/tags`, `/v1beta/models`, `/v1/models`) see every collection in both
208
+ (`/api/tags`, `/v1beta/models`, `/v1/models`) see every collection in all three
209
209
  reply modes.
210
210
 
211
211
  | Format | Routes | Streams with | `bench` field | Known clients |
@@ -269,12 +269,15 @@ bytes), Gemini `inlineData` and `fileData` at an http(s) URL.
269
269
  **Refused, not dropped** (400, with why), because a client waiting for what
270
270
  it asked would hang or loop on a panel's text:
271
271
 
272
- - **Tools and agents are not supported yet.** A request carrying tools, or a
273
- conversation holding tool calls or tool results, in any format (chat
274
- completions included), is refused with a message saying a route mode is
275
- coming: one member proxied with tools and streaming, for agent clients
276
- such as Claude Code, Codex or opencode, with background shadow seats as an
277
- opt-in after that. An empty `tools` list is no tools.
272
+ - **Tools, in a panel.** A request carrying tools, or a conversation
273
+ holding tool calls or tool results, in any format, is refused by a panel
274
+ (`all` or `merged`) with a message pointing at route mode (next section),
275
+ which takes them. An empty `tools` list is no tools.
276
+ - a tool only its own vendor runs (Anthropic's server tools such as
277
+ `web_search_20250305` and their result blocks, OpenAI's hosted tools such
278
+ as `web_search`, Gemini's Google Search, URL context and code execution),
279
+ in route mode too: no other model can run them;
280
+ - the legacy `functions` / `function_call` fields: send `tools`;
278
281
  - a PDF, audio or other non-image part; an image as an uploaded file id
279
282
  (Anthropic `file`, Responses `file_id`, Gemini's Files API);
280
283
  - `previous_response_id` or a stored `conversation` on Responses: nothing is
@@ -282,12 +285,166 @@ it asked would hang or loop on a panel's text:
282
285
  - Ollama's `context` (a model's own token state): use `/api/chat`;
283
286
  - more than one prompt on legacy completions.
284
287
 
285
- **Ignored**: sampling options (`max_tokens`, `temperature`, `top_p`, `stop`
286
- and their native names) are read but each seat keeps its own call options;
287
- structured-output settings (Ollama `format`, Gemini `responseSchema`,
288
- Responses `text.format`), since a panel answers in markdown; a model's own
288
+ **Ignored by a panel**: sampling options (`max_tokens`, `temperature`,
289
+ `top_p`, `stop` and their native names) are read but each seat keeps its own
290
+ call options; structured-output settings (Ollama `format`, Gemini
291
+ `responseSchema`, Responses `text.format`), since a panel answers in
292
+ markdown. Route mode passes both on. **Ignored everywhere**: a model's own
289
293
  reasoning from an earlier turn (Anthropic thinking blocks, Responses
290
- reasoning items, Gemini thought parts).
294
+ reasoning items, Gemini thought parts), and native parameters with no
295
+ OpenAI counterpart (Anthropic's `top_k`, `thinking`, `metadata`).
296
+
297
+ ## Route mode (agent clients)
298
+
299
+ A panel answers with several models' text; an agent needs one model that
300
+ calls its tools. **Route mode** is that: no panel, one member of the
301
+ collection gets the request as sent — messages, tool calls and results,
302
+ `tools`, `tool_choice`, `parallel_tool_calls`, `response_format`,
303
+ temperature and the rest, images — and its answer is relayed in the
304
+ request's own format, streamed or not. Ask for it with `:route` on the
305
+ model (`bench/core:route`), `"bench": {"reply": "route"}`, or set a
306
+ collection's reply to *route* in the Collections dialog. `GET /v1/models`
307
+ (and `/api/tags`, `/v1beta/models`) lists every collection in all three
308
+ modes, so an agent's model picker can choose `bench/core:route`.
309
+
310
+ **Who answers.** Members only, no explorers: an agent's working session is
311
+ not the place to try an unknown model. Excluded, and named in
312
+ `bench.skipped` with why, as a panel excludes them: retired and ad hoc
313
+ members, members whose endpoint speaks another API than chat completions,
314
+ members the catalog does not know to call tools (when the request has
315
+ tools; the model popup's *Tool calling* is the override, like *Vision /
316
+ OCR*; the catalog's `tools` comes from OpenRouter's listed parameters,
317
+ models.dev, the endpoint's own listing or a capability probe that saw a
318
+ call), that cannot see (when it has images) or whose context window is
319
+ under the request (tools and tool calls counted). No member left: 400
320
+ (413 when size is why). Of those left, the best record goes first: useful
321
+ verdicts counted, then verified suite findings, then a connection already
322
+ paid for; members cooling down after a rate limit go last. One new
323
+ conversation in four goes instead to the member whose record is oldest, so
324
+ the record does not narrow to one model.
325
+
326
+ **Sticky.** A conversation stays on its model across turns: switching
327
+ mid-task breaks the agent's coherence and the provider's prompt cache. The
328
+ key is a hash of the collection, the gateway key, the system text and the
329
+ first user message, which an agent resends unchanged every turn; it lives
330
+ in memory and lapses after an hour idle (or a restart). A side request with
331
+ its own system prompt (Claude Code's command checks, a subagent, a title) is
332
+ its own conversation. If the conversation's model cannot take a turn (an
333
+ image arrives), the turn is re-ranked; the model that answers becomes the
334
+ conversation's.
335
+
336
+ **Failover.** If the member fails before the first piece of its answer
337
+ reaches the caller — a 429, a 5xx, a pacing refusal, nothing at all in 90
338
+ seconds, an empty reply (even after reasoning) — the next member in order
339
+ gets the turn, transparently, up to three; the caller sees only the answer,
340
+ and `bench.failed_over` names who failed and why. Reasoning streamed before
341
+ the answer is held back with it. A streamed turn's 200 waits up to 15
342
+ seconds for that first piece, so a turn that fails everywhere quickly keeps
343
+ its status (429 when every member was rate limited, else 502), then
344
+ keep-alives cover the wait. After the first piece, an error goes into the
345
+ stream in the format's error shape: no other model can continue a
346
+ half-written answer.
347
+
348
+ **Streams.** OpenAI chat completions relays the member's chunks as they
349
+ came, tool-call deltas exactly, under the collection's name, then a closing
350
+ chunk with no choice that carries `bench`. The other formats stream the
351
+ text as it comes and each tool call **whole when the turn ends** (an
352
+ Anthropic `tool_use` block with one `input_json_delta`, a Responses
353
+ `function_call` item, a Gemini `functionCall` part, an Ollama `tool_calls`
354
+ line), so calls whose deltas interleave still arrive intact. Usage is the
355
+ member's own count. An output budget over the model's known maximum is cut
356
+ to it (Claude Code asks every model for 32,000). Headers name the
357
+ conversation (`X-Bench-Conversation`) and, when known before the 200, the
358
+ model (`X-Bench-Model`).
359
+
360
+ **Translations.** Anthropic `tool_use` / `tool_result` blocks, Responses
361
+ `function_call` / `function_call_output` items, Gemini `functionCall` /
362
+ `functionResponse` parts and Ollama `tool_calls` / `tool` messages become
363
+ OpenAI tool calls and tool messages and back; tool definitions and
364
+ `tool_choice` likewise (Gemini's upper-case schema types lowered). Tool call
365
+ ids pass through both ways; Gemini and Ollama usually send none, so they
366
+ are numbered per request and a result matched to the oldest open call of
367
+ its name. A Responses **custom tool** (Codex's free-form `apply_patch`)
368
+ becomes a function taking one string, `input`, its grammar in the
369
+ description, and the call goes back as a `custom_tool_call`. An image in an
370
+ Anthropic tool result goes in a user message after it (a tool message
371
+ carries text only); `is_error` prefixes the result with `Error: `.
372
+
373
+ **Recording (the decision).** Every call is in the usage ledger (purpose
374
+ `route`), and every turn a line in `runs/gateway/routes.jsonl`: model,
375
+ status, seconds, tool calls, tokens, who failed over — never the prompt,
376
+ which an agent resends whole every turn. For the router, a conversation is
377
+ **one unjudged offload per model that answered it** (call id
378
+ `<conversation>.1`, `.2` after a failover to another model), not one per
379
+ turn: an agent session is dozens of turns, and a seat per turn would bury
380
+ the panels' verdicts. A verdict on a conversation is optional
381
+ (`POST /bench/verdict` with that call id); the offload alone already counts
382
+ toward rotation. The Gateway card lists route conversations in their own
383
+ table (when, who asked, the first message, who answered with ✓/✗, turns,
384
+ tool calls, failovers) and none of them adds to **Owed**.
385
+
386
+ **Shadows (later, opt-in).** A shadow seat would answer the same turn in the
387
+ background for evaluation only, after the caller has its answer. The place
388
+ is marked in `route_turn` ("SHADOWS GO HERE"): each would be its own client
389
+ call with purpose `shadow`, recorded on the conversation's call id; nothing
390
+ the caller sees would wait for or change because of them. Not built.
391
+
392
+ **Limits, honestly.**
393
+
394
+ - Members reached through native Anthropic, Gemini or Responses endpoints
395
+ are not routed to: route mode speaks chat completions to the member.
396
+ - The member is a free or flat-rate model, not the model the client was
397
+ built for. Claude Code and Codex are tuned for their own vendors' models;
398
+ smaller models make more tool-call mistakes, and some providers reject a
399
+ tool schema another accepts (that request fails over).
400
+ - Prompt caching is the provider's own: stickiness keeps the prefix on one
401
+ provider, but `cache_control` marks are not passed on.
402
+ - Thinking is not shown to Anthropic, Responses, Gemini or Ollama clients
403
+ (a model's reasoning is relayed raw only in chat completions).
404
+ - Tool calls arrive whole, not as deltas, outside chat completions.
405
+ - A caller that hangs up does not stop the member; its answer runs out and
406
+ is recorded.
407
+ - Stickiness is per gateway process, in memory.
408
+
409
+ ### Pointing agents at a collection
410
+
411
+ The base URL is the server: `http://127.0.0.1:8122` (the dashboard's port)
412
+ or `:8123` (`gateway.py`). On this machine any key text works; elsewhere,
413
+ or where keys are required, use a gateway key from Settings.
414
+
415
+ **Claude Code** (Anthropic Messages). It sends the model named by
416
+ `ANTHROPIC_MODEL` (or `--model`) for its main loop and a smaller "haiku"
417
+ model for side tasks (command checks, titles), so set both; a name it does
418
+ not know prints a harmless `unrecognized_model` note:
419
+
420
+ ```bash
421
+ export ANTHROPIC_BASE_URL=http://127.0.0.1:8122
422
+ export ANTHROPIC_API_KEY=<a gateway key, or any text on this machine>
423
+ export ANTHROPIC_MODEL=bench/core:route
424
+ export ANTHROPIC_DEFAULT_HAIKU_MODEL=bench/fast:route
425
+ export ANTHROPIC_DEFAULT_SONNET_MODEL=bench/core:route ANTHROPIC_DEFAULT_OPUS_MODEL=bench/core:route
426
+ export CLAUDE_CODE_SUBAGENT_MODEL=bench/core:route
427
+ claude
428
+ ```
429
+
430
+ Checked end to end (2026-10-23): Claude Code 2.1.299 ran Read, Write and
431
+ Bash tool loops through `bench/core:route` and `bench/fast:route`, failing
432
+ over past a member's 429 on the first turn and staying on one model after.
433
+
434
+ **Codex and other Responses clients**: a provider with `base_url =
435
+ "http://127.0.0.1:8122/v1"`, `wire_api = "responses"`, the model
436
+ `bench/core:route`, and the gateway key as its API key. Function and custom
437
+ tools work; hosted tools (`web_search`, `local_shell`) are refused, so turn
438
+ them off. Nothing is stored: `previous_response_id` is refused, so the
439
+ client must send the whole conversation every turn.
440
+ Not checked against a real Codex yet.
441
+
442
+ **opencode, Cline, Continue, Roo, Aider and other OpenAI-compatible agents**:
443
+ an OpenAI-compatible provider at `http://127.0.0.1:8122/v1`, model
444
+ `bench/core:route` (it is in `/v1/models`). Agents that speak Ollama
445
+ (`http://127.0.0.1:8122`, model `bench/core:route`) work the same way. A
446
+ real `curl` with tools streamed a model's tool-call deltas through
447
+ unchanged; these clients themselves were not run.
291
448
 
292
449
  **Counts and durations.** `usage` and the token-count routes
293
450
  (`/v1/messages/count_tokens`, Gemini `:countTokens`) are the size check's
package/README.md CHANGED
@@ -48,7 +48,7 @@ verdicts decide who sits on the next panel.
48
48
 
49
49
  | Related tools | Examples | What they do | What Benchwarmers does differently |
50
50
  |---|---|---|---|
51
- | Gateways and routers | LiteLLM, OpenRouter, Portkey | One request to one model, with fallback | Fans one prompt out to a panel and returns every answer |
51
+ | Gateways and routers | LiteLLM, OpenRouter, Portkey | One request to one model, with fallback | Fans one prompt out to a panel and returns every answer; for agents, routes to one member by its record |
52
52
  | Councils and ensembles | LLM Council, Mixture-of-Agents | Several models merged into one answer | Keeps every answer, because who said what is the evidence; one merged summary only if you ask |
53
53
  | Side-by-side chat | Open WebUI, LMArena | Several answers shown together | Records a verdict per answer and uses them to pick the next panel |
54
54
  | Evaluation harnesses | promptfoo, Inspect, SWE-bench | Score models offline on fixed test sets | Scores them on your real work and your own code, and keeps rotating new models in |
@@ -294,6 +294,16 @@ On native Windows, a client that cannot start `npx` itself needs
294
294
  | `BENCHWARMERS_KEY` | a gateway key from Settings, sent as `Authorization: Bearer`; needed for a server on another machine, or where keys are required |
295
295
  | `BENCHWARMERS_TIMEOUT` | seconds to wait for a panel, default 900 |
296
296
 
297
+ **An agent running on a collection.** The MCP tools ask a panel for a
298
+ second opinion; an agent can also *be driven by* a collection, in **route
299
+ mode** (`bench/<collection>:route`): one member, picked by its record, gets
300
+ each turn with its tools, stays on for the whole conversation, and is
301
+ replaced by the next member if it fails before answering. Claude Code
302
+ points at it with `ANTHROPIC_BASE_URL=http://127.0.0.1:8122` and
303
+ `ANTHROPIC_MODEL=bench/core:route`; Codex, opencode, Cline and other
304
+ OpenAI-, Responses- or Ollama-speaking agents with the same base URL.
305
+ [GATEWAY.md](GATEWAY.md), "Route mode", has the set-up and the limits.
306
+
297
307
  <!-- The MCP Registry's proof that the PyPI package is this server (server.json):
298
308
  mcp-name: io.github.WendellResearch-AI/benchwarmers -->
299
309
 
package/USAGE.md CHANGED
@@ -116,9 +116,24 @@ its base URL and a collection as its model:
116
116
  | Older tools on `/v1/completions` | `http://127.0.0.1:8122/v1` | one `prompt` |
117
117
 
118
118
  Asked to stream, each answers at once and sends each seat's answer as it
119
- finishes. Agents that need tools (Claude Code, Codex, opencode as callers)
120
- are not supported yet: a request with tools is refused with a 400 saying so.
121
- GATEWAY.md, "Other API formats", has a curl for each.
119
+ finishes. GATEWAY.md, "Other API formats", has a curl for each.
120
+
121
+ **An agent that calls tools** (Claude Code, Codex, opencode, Cline...) uses
122
+ a collection's **route mode**: model `bench/<collection>:route`. No panel:
123
+ one member, picked by its record, gets the request with its tools and
124
+ streams its answer back in the agent's own format; the conversation stays
125
+ on that model, and a member that fails before answering is replaced by the
126
+ next. A panel refuses tools with a 400 saying so. For Claude Code:
127
+
128
+ ```bash
129
+ export ANTHROPIC_BASE_URL=http://127.0.0.1:8122 ANTHROPIC_API_KEY=local
130
+ export ANTHROPIC_MODEL=bench/core:route ANTHROPIC_DEFAULT_HAIKU_MODEL=bench/fast:route
131
+ claude
132
+ ```
133
+
134
+ Each conversation is one offload per model that answered it; a verdict is
135
+ optional, and the Gateway card lists route conversations apart from the
136
+ panels. GATEWAY.md, "Route mode", has the other clients and the limits.
122
137
 
123
138
  **A coding agent** connects over MCP instead: the Claude Code plugin
124
139
  (`/plugin marketplace add WendellResearch-AI/benchwarmers`, then
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.2
1
+ 0.3.0
package/cli.py CHANGED
@@ -615,7 +615,7 @@ def parser() -> argparse.ArgumentParser:
615
615
  c.add_argument("--shape", help="the task shape explorers are picked for; empty for automatic")
616
616
  c.add_argument("--description", help="what it is for: callers read it to choose")
617
617
  c.add_argument("--tags", help="comma-separated")
618
- c.add_argument("--reply", choices=["all", "merged"])
618
+ c.add_argument("--reply", choices=["all", "merged", "route"])
619
619
  c.add_argument("--merger", help="the alias that merges, for --reply merged")
620
620
 
621
621
  cmd("endpoints", cmd_endpoints, "where models are served from")