@wendellresearch-ai/benchwarmers 0.5.3 → 0.5.4

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.5.3
1
+ 0.5.4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendellresearch-ai/benchwarmers",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
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",
@@ -461,6 +461,15 @@ def main() -> int:
461
461
  print(("ok " if ok else "FAIL ") + "action needed drops alerts on retired models")
462
462
  failures += 0 if ok else 1
463
463
 
464
+ # A provider's words end where its link begins, and are not cut short here:
465
+ # the page cuts them to a line, and a phone, with no hover, shows them whole.
466
+ text, url = serve.said("Error: Error code: 403 - {'error': {'message': 'm needs your consent. Give it once at"
467
+ " https://p.example/consents?model=m; it covers every model.'}}")
468
+ long, _ = serve.said("Error: Error code: 429 - {'error': {'message': 'Too many requests. " + "w " * 80 + "end'}}")
469
+ ok = text.endswith("Give it once at") and url == "https://p.example/consents?model=m" and long.endswith("end")
470
+ print(("ok " if ok else "FAIL ") + "action needed shows a provider's words whole, its link after them")
471
+ failures += 0 if ok else 1
472
+
464
473
  # Dismiss: one picker key or many in one write, a change-log line each;
465
474
  # undo brings back only the ones named; nothing named is refused.
466
475
  serve.DISMISSED_PATH = cfg / "dismissed.json"
package/serve.py CHANGED
@@ -2007,7 +2007,9 @@ def said(detail: str) -> tuple[str, str]:
2007
2007
  """An error as one short line, and the link the provider put in it: the
2008
2008
  status code and the provider's own words, without the JSON around them or
2009
2009
  a lead-in such as "Too many requests." that says less than the sentence
2010
- after it. The whole detail stays in the row's title."""
2010
+ after it, ending where the link it gives begins (the link follows it on
2011
+ the page). Cut to a line by the page, not here: a phone shows it whole.
2012
+ The whole detail stays in the row's title."""
2011
2013
  code = re.search(r"Error code: (\w+)", detail)
2012
2014
  quoted = re.search(r"""['"](?:message|detail)['"]: ['"](.+?)(?:['"]\s*[,}]|$)""", detail)
2013
2015
  text = (quoted.group(1) if quoted else re.sub(r"^.*?Error: ", "", detail) if "Error: " in detail
@@ -2017,8 +2019,16 @@ def said(detail: str) -> tuple[str, str]:
2017
2019
  if rest and len(first) < 25:
2018
2020
  text = rest
2019
2021
  url = re.search(r"https?://[^\s;,'\"()]+", quoted.group(1) if quoted else "")
2022
+ if url and url.group(0) in text:
2023
+ text = text[:text.index(url.group(0))].rstrip()
2020
2024
  text = (f"{code.group(1)} · " if code else "") + text
2021
- return (text if len(text) <= 110 else text[:109].rstrip() + "…"), (url.group(0) if url else "")
2025
+ return (text if len(text) <= 400 else text[:399].rstrip() + "…"), (url.group(0) if url else "")
2026
+
2027
+
2028
+ def breakable(name: str) -> str:
2029
+ """An escaped name that may wrap at a domain's dots (`…-api.unorouter.com`),
2030
+ never inside a version number (`llama-3.2`)."""
2031
+ return re.sub(r"\.(?=[a-z])", ".<wbr>", name)
2022
2032
 
2023
2033
 
2024
2034
  def open_actions() -> tuple[dict[str, dict], list[tuple[str, dict]]]:
@@ -2128,7 +2138,7 @@ def actions_page(groups: dict, auto: list) -> str:
2128
2138
  f"<span class='acts'>{got}</span></div>")
2129
2139
  alias = esc(r["alias"])
2130
2140
  return (f"<div class='a-row' data-row><a href='#' class='model' data-alias='{alias}'>"
2131
- f"<span class='alias'>{alias}</span></a><span>{chip(r['label'], r['css'])}</span>"
2141
+ f"<span class='alias'>{breakable(alias)}</span></a><span>{chip(r['label'], r['css'])}</span>"
2132
2142
  f"<span class='why' title='{esc(r['title'])}'><span class='said'>{esc(r['said'])}</span>{link(r['url'])}</span>"
2133
2143
  f"<span class='acts'><button class='linkish' data-act='retry' data-alias='{alias}'"
2134
2144
  " title='One call now; an answer clears the alert'>Retry</button>"
@@ -2155,7 +2165,7 @@ def actions_page(groups: dict, auto: list) -> str:
2155
2165
  tags = load_json(DATA / "config" / "provider-tags.json") # a retired model's connection, by served name
2156
2166
  gone = "".join(
2157
2167
  f"<div class='a-row' data-row><a href='#' class='model' data-alias='{esc(alias)}'>"
2158
- f"<span class='alias'>{esc(alias)}</span></a><span class='when'>{esc(r['at'][:10])}</span>"
2168
+ f"<span class='alias'>{breakable(esc(alias))}</span></a><span class='when'>{esc(r['at'][:10])}</span>"
2159
2169
  f"<span class='why' title='{esc(r.get('why', ''))}'><span class='said'>"
2160
2170
  f"{esc(tags.get(r.get('served', ''), '').strip('?') or r.get('endpoint') or endpoints.default())}"
2161
2171
  f" · {esc(said(r.get('why', '').removeprefix('auto: '))[0])}</span></span>"
package/ui/bench.css CHANGED
@@ -132,18 +132,21 @@ main { max-width: var(--page, 104rem); margin: 0 auto; padding: 0 var(--gutter)
132
132
  background: var(--warn-ground); border-radius: var(--radius); font-size: .9375rem; overflow-wrap: anywhere; }
133
133
  .a-held .chip { flex: none; background: var(--ink); color: var(--paper); }
134
134
  .a-held a { color: var(--ink); font-weight: 500; }
135
- .a-rows { display: grid; grid-template-columns: minmax(10rem, max-content) max-content minmax(0, 1fr) max-content;
135
+ /* The same fixed columns in every group and the retired list, so state,
136
+ words and actions line up down the whole page whatever each group's
137
+ longest name; a long name wraps in its column. */
138
+ #tab-needs > .panel { container-type: inline-size; }
139
+ .a-rows { display: grid; grid-template-columns: minmax(0, 32%) 8.25rem minmax(0, 1fr) 6.25rem;
136
140
  gap: 0 1rem; align-items: baseline; }
137
141
  .a-row { display: contents; }
138
142
  .a-row > * { padding: .3rem 0; border-top: 1px solid color-mix(in oklab, var(--line) 55%, transparent); }
139
143
  .a-row:first-child > * { border-top: 0; }
140
- .a-row .alias, .a-row .what { font-weight: 500; }
141
- .a-row .what { max-width: 24rem; } /* a notice's sentence wraps, leaving its detail room */
144
+ .a-row .alias, .a-row .what { font-weight: 500; overflow-wrap: anywhere; } /* a name breaks at its dots first (<wbr>) */
142
145
  .a-row .why, .a-row .when { color: var(--muted); font-size: .875rem; overflow-wrap: anywhere; }
143
- /* One line a model: the provider's words cut to the column, whole on hover; its link stays whole. */
146
+ /* One line a model: the provider's words cut to the column, whole on hover; the link gives way last. */
144
147
  .a-row .why:has(> .said) { display: flex; gap: .5rem; min-width: 0; }
145
- .a-row .said { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
146
- .a-row .why > a { flex: none; }
148
+ .a-row .said { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
149
+ .a-row .why > a { flex: none; max-width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
147
150
  .a-row .acts { display: flex; gap: .9rem; font-size: .875rem; }
148
151
  .linkish.danger { color: var(--err); }
149
152
  .a-retired { margin-top: .75rem; border-top: 1px solid var(--line); padding-top: .5rem; }
@@ -542,11 +545,18 @@ dialog .pick { display: flex; gap: 1.5rem; }
542
545
  button.c-row { grid-template-columns: minmax(0, 1fr); } /* the seats get the width; the whole row opens */
543
546
  .c-row .open-hint { display: none; }
544
547
  .a-line { margin: 1rem 1rem 0; }
545
- .a-rows { grid-template-columns: minmax(0, 1fr) max-content; } /* name and state; the words and actions under them */
548
+ }
549
+ /* A narrow page (a phone, or a window with the sidebar beside it): name and
550
+ state on one line, the words and actions under them. No gap between name
551
+ and state, so the line above a model runs unbroken. */
552
+ @container (max-width: 46rem) {
553
+ .a-rows { grid-template-columns: minmax(0, 1fr) max-content; column-gap: 0; }
554
+ .a-row > :nth-child(2) { padding-left: .75rem; }
546
555
  .a-row .why, .a-row .acts { grid-column: 1 / -1; padding-top: 0; border-top: 0; }
547
556
  .a-row .acts { padding-bottom: .5rem; }
548
557
  .a-row .why:has(> .said) { flex-wrap: wrap; }
549
- .a-row .said { white-space: normal; } /* a phone has the height to read it whole */
558
+ .a-row .said { white-space: normal; } /* the height to read it whole */
559
+ .a-row .why > a { max-width: none; }
550
560
  }
551
561
  @media (prefers-reduced-motion: reduce) {
552
562
  * { transition: none !important; }