@wendellresearch-ai/benchwarmers 0.6.7 → 0.6.8

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.7
1
+ 0.6.8
package/metrics/decide.py CHANGED
@@ -19,12 +19,17 @@ model answers a typed question.
19
19
 
20
20
  WHERE THE CALL GOES
21
21
  -------------------
22
- Each endpoint in `config/endpoints.json` may carry a `decisions` URL. Open
22
+ Each endpoint in `config/endpoints.json` may carry a `decisions` URL. One
23
+ that is not a hub and has none is asked under its own base (`decisions_url`:
24
+ `<base>/decisions`, then `<base>/systemone`, as AMD Token Lab names it). Open
23
25
  WebUI has one only through its `/openai/{path}` passthrough, which forwards to
24
26
  `{connection base}/decisions` and answers only when the server runs with
25
27
  ENABLE_OPENAI_API_PASSTHROUGH=True. That reaches Inception
26
28
  (`api.inceptionlabs.ai/v1/decisions`) but never OpenRouter, whose Decisions API
27
- is `/api/alpha/decisions`, outside its `/api/v1` base.
29
+ is `/api/alpha/decisions`, outside its `/api/v1` base. Nor a connection with
30
+ a prefix in Open WebUI: the passthrough sends the provider the prefixed id
31
+ (`amd.d3-flash`), which it does not know; such a model is onboarded from the
32
+ provider's own endpoint.
28
33
 
29
34
  A model is called on the endpoint it was picked from when onboarding, recorded
30
35
  as `via`: picked from Open WebUI, it goes through Open WebUI; picked from
@@ -226,6 +231,16 @@ def load() -> dict[str, dict]:
226
231
  return {}
227
232
 
228
233
 
234
+ def decisions_url(endpoint: str) -> str:
235
+ """Where `endpoint`'s Decisions API is: its own `decisions` URL, else `{path}`
236
+ under its base, which is where a provider that has one serves it (AMD Token
237
+ Lab: /v1/systemone), so a direct endpoint needs nothing written by hand. ""
238
+ for a hub without one: its relay is not under its base (Open WebUI's is
239
+ /openai/{path}, outside /api) and is given by hand."""
240
+ url = endpoints.load()[endpoint].get("decisions") or ""
241
+ return url or ("" if endpoints.hub(endpoint) else endpoints.base(endpoint) + "/{path}")
242
+
243
+
229
244
  def call(endpoint: str, served: str, state, questions: dict, timeout: int = 60,
230
245
  path: str = "decisions", alias: str = "", use: str = "", meta: dict | None = None,
231
246
  retry: bool = True) -> dict:
@@ -237,7 +252,7 @@ def call(endpoint: str, served: str, state, questions: dict, timeout: int = 60,
237
252
  try:
238
253
  # Inside the try, so an endpoint gone from the config is logged too.
239
254
  entry = endpoints.load()[endpoint]
240
- url = (entry.get("decisions") or "").replace("{path}", path)
255
+ url = decisions_url(endpoint).replace("{path}", path)
241
256
  if not url:
242
257
  raise ValueError(f"{endpoint} has no decisions URL in config/endpoints.json")
243
258
  headers = {"Content-Type": "application/json", **(entry.get("headers") or {})}
@@ -343,7 +358,7 @@ def error_text(exc: Exception) -> str:
343
358
  def probe(endpoint: str, served: str) -> tuple[str | None, str]:
344
359
  """(None, path) if `endpoint`'s decisions URL answers a yes/no question for
345
360
  `served`, else (why not, ""). Each of PATHS is tried where the URL has `{path}`."""
346
- url = endpoints.load()[endpoint].get("decisions")
361
+ url = decisions_url(endpoint)
347
362
  if not url:
348
363
  return f"{endpoint} has no decisions URL in config/endpoints.json", ""
349
364
  why = []
@@ -20,7 +20,8 @@ that asks "what is served" asks every endpoint here.
20
20
 
21
21
  `decisions` (optional) is the full URL of the endpoint's Decisions API, for
22
22
  decision models that cannot be reached through chat completions; see
23
- `metrics/decide.py`. Open WebUI's is its `/openai/{path}` passthrough, which
23
+ `metrics/decide.py`. Without one, an endpoint that is not a hub is asked under
24
+ its own base (`decide.decisions_url`). Open WebUI's is its `/openai/{path}` passthrough, which
24
25
  answers only when the server runs with ENABLE_OPENAI_API_PASSTHROUGH=True;
25
26
  `{path}` is filled per model (`decisions` for Inception, `systemone` for
26
27
  Ollama), because the passthrough appends it to the connection's own base.
@@ -31,7 +31,9 @@ def jsonl(path: str, rows: list[dict]) -> None:
31
31
  write("config/endpoints.json", {"hub": {"base": "http://127.0.0.1:9/api", "key": "hub", "default": True,
32
32
  "decisions": "http://127.0.0.1:9/openai/{path}"},
33
33
  "cf": {"base": "http://127.0.0.1:9/ai/v1", "key": "hub",
34
- "decisions": "http://127.0.0.1:9/ai/run/{model}"}})
34
+ "decisions": "http://127.0.0.1:9/ai/run/{model}"},
35
+ "bare": {"base": "http://127.0.0.1:9/v1", "key": "hub", "kind": "provider"},
36
+ "barehub": {"base": "http://127.0.0.1:8/api", "key": "hub"}})
35
37
  # dm2 first: before any result it is the one asked (the first that takes choice, as before),
36
38
  # except per request, where its :free budget is spared while dm1 can answer.
37
39
  MODELS = {
@@ -148,6 +150,20 @@ decide.urllib.request.urlopen = cf_urlopen
148
150
  assert decide.probe("cf", "@cf/cloudflare/clef-flash") == (None, "decisions")
149
151
  assert decide.log_rows()[-1]["answers"]["probe"]["noul"] == 0.98
150
152
  decide.LOG.unlink()
153
+
154
+
155
+ # An endpoint with no decisions URL is asked under its own base, each path in turn (AMD Token
156
+ # Lab answers only /v1/systemone); a hub without one is not asked: its relay is given by hand.
157
+ def bare_urlopen(request, timeout=0):
158
+ if request.full_url != "http://127.0.0.1:9/v1/systemone":
159
+ raise http_error(b'{"error": {"message": "Unknown endpoint"}}', 404)
160
+ return io.BytesIO(json.dumps({"answers": {"probe": {"type": "noul", "noul": 0.99}}}).encode())
161
+
162
+
163
+ decide.urllib.request.urlopen = bare_urlopen
164
+ assert decide.probe("bare", "d3") == (None, "systemone")
165
+ assert decide.probe("barehub", "d3") == ("barehub has no decisions URL in config/endpoints.json", "")
166
+ decide.LOG.unlink()
151
167
  decide.urllib.request.urlopen = urlopen
152
168
 
153
169
  # ---- every call is logged, and an error's body survives the log -------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendellresearch-ai/benchwarmers",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Self-hosted panel gateway for LLMs: ask a collection of models at once, keep every answer side by side, rotate explorer models in, and let your verdicts pick the next panel.",
5
5
  "mcpName": "io.github.WendellResearch-AI/benchwarmers",
6
6
  "license": "MIT",
@@ -292,8 +292,16 @@ def main() -> None:
292
292
  " models_url .../ai/models/search?per_page=1000) and onboard it from there.\n"
293
293
  if "@cf/" in name and endpoint != "cloudflare" and "{model}" not in
294
294
  (endpoints.load()[endpoint].get("decisions") or "") else
295
+ # Only a hub gets here: any other endpoint is asked under its own base.
296
+ " Give this hub a decisions URL in config/endpoints.json (Open WebUI's is"
297
+ " http://<host>/openai/{path}), or onboard the model from its provider's own endpoint.\n"
298
+ if "has no decisions URL" in why else
295
299
  " Open WebUI relays decisions only with ENABLE_OPENAI_API_PASSTHROUGH=True"
296
300
  " in its environment.\n" if owui and "passthrough" in why.lower() else
301
+ # The relay sends the id as Open WebUI lists it, connection prefix and all.
302
+ f" Open WebUI sends the provider the prefixed id ({name}), which it does not know."
303
+ " Add the provider as an endpoint of its own and onboard the model from there.\n"
304
+ if owui and " 404" in why and connection_prefix(live[name]) else
297
305
  " Its connection has no /decisions under its base. An OpenRouter-hosted decision"
298
306
  " model cannot go through Open WebUI (its Decisions API is outside /api/v1);"
299
307
  " pick it from openrouter (direct) instead.\n" if owui and " 404" in why and " 402" not in why else