pokertools-arena 0.16.2 → 0.17.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/README.md +1 -1
- package/dist/app.js +108 -14
- package/dist/index.html +5 -0
- package/dist/pokertools-arena.html +114 -15
- package/dist/styles.css +1 -1
- package/docs/diagnostics/SUMMARY.md +1 -1
- package/docs/diagnostics/summary.json +1 -1
- package/docs/releases/RELEASE_NOTES.md +32 -0
- package/package.json +1 -1
- package/src/app.js +57 -3
- package/src/index.html +5 -0
- package/src/lib/decision-core.js +78 -8
- package/src/styles.css +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| **Package** | `npx pokertools-arena` |
|
|
18
18
|
| **Runtime** | Node.js ≥ 24 for tooling; any modern browser for the app |
|
|
19
19
|
|
|
20
|
-
> **Current release: 0.
|
|
20
|
+
> **Current release: 0.17.0.** Seats on OpenRouter models that advertise reasoning efforts can now pick a per-seat reasoning effort, so a slow reasoner such as DeepSeek can be lowered from its `high` default to `low` instead of exhausting the action clock and being auto-folded.
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
package/dist/app.js
CHANGED
|
@@ -114432,8 +114432,15 @@ async function fetchStreamingJson(url, options, { maxRetries = 1, onDelta = null
|
|
|
114432
114432
|
throw new ArenaRequestError(message, { status: response.status, category: requestErrorCategory(response.status), payload, incidents });
|
|
114433
114433
|
}
|
|
114434
114434
|
}
|
|
114435
|
+
function requestErrorDetail(err) {
|
|
114436
|
+
const parts = [summarizeError(err)];
|
|
114437
|
+
const nested = err?.payload?.error?.metadata?.raw ?? err?.payload?.metadata?.raw;
|
|
114438
|
+
if (typeof nested === "string") parts.push(nested);
|
|
114439
|
+
else if (nested) parts.push(String(JSON.stringify(nested)));
|
|
114440
|
+
return parts.join(" ");
|
|
114441
|
+
}
|
|
114435
114442
|
function isUnsupportedToolChoiceError(err) {
|
|
114436
|
-
const message =
|
|
114443
|
+
const message = requestErrorDetail(err);
|
|
114437
114444
|
return [400, 404, 422].includes(Number(err?.status)) && /tool[_ -]?choice|tool call|tools?.*(?:unsupported|support)|no endpoints?.*support/i.test(message);
|
|
114438
114445
|
}
|
|
114439
114446
|
function mapGet(mapish, key, fallback = 0) {
|
|
@@ -114459,13 +114466,23 @@ function jsonSafe(value) {
|
|
|
114459
114466
|
}
|
|
114460
114467
|
var protocolCapabilityCache = /* @__PURE__ */ new Map();
|
|
114461
114468
|
var modelSupportedParameters = /* @__PURE__ */ new Map();
|
|
114469
|
+
var modelReasoningCapabilities = /* @__PURE__ */ new Map();
|
|
114462
114470
|
var DEFAULT_TEMPERATURE = 0.3;
|
|
114463
114471
|
function registerModelCapabilities(models = []) {
|
|
114464
114472
|
for (const entry of models ?? []) {
|
|
114465
114473
|
const id2 = typeof entry === "string" ? entry : entry?.id;
|
|
114474
|
+
if (!id2) continue;
|
|
114466
114475
|
const parameters = typeof entry === "object" && entry ? entry.supported_parameters : null;
|
|
114467
|
-
if (
|
|
114468
|
-
|
|
114476
|
+
if (Array.isArray(parameters)) modelSupportedParameters.set(String(id2), new Set(parameters.map(String)));
|
|
114477
|
+
const reasoning = typeof entry === "object" && entry ? entry.reasoning : null;
|
|
114478
|
+
if (!reasoning || typeof reasoning !== "object") continue;
|
|
114479
|
+
const efforts = Array.isArray(reasoning.supported_efforts) ? reasoning.supported_efforts.map((value) => String(value).trim().toLowerCase()).filter(Boolean) : [];
|
|
114480
|
+
const declaredDefault = String(reasoning.default_effort || "").trim().toLowerCase();
|
|
114481
|
+
modelReasoningCapabilities.set(String(id2), {
|
|
114482
|
+
mandatory: reasoning.mandatory === true,
|
|
114483
|
+
supportedEfforts: efforts,
|
|
114484
|
+
defaultEffort: efforts.includes(declaredDefault) ? declaredDefault : null
|
|
114485
|
+
});
|
|
114469
114486
|
}
|
|
114470
114487
|
}
|
|
114471
114488
|
function modelSupportsParameter(model, parameter) {
|
|
@@ -114473,6 +114490,34 @@ function modelSupportsParameter(model, parameter) {
|
|
|
114473
114490
|
if (!supported) return null;
|
|
114474
114491
|
return supported.has(String(parameter));
|
|
114475
114492
|
}
|
|
114493
|
+
function reasoningCapabilities(model) {
|
|
114494
|
+
return modelReasoningCapabilities.get(String(model || "")) ?? null;
|
|
114495
|
+
}
|
|
114496
|
+
function reasoningEffortsFor(model) {
|
|
114497
|
+
return reasoningCapabilities(model)?.supportedEfforts ?? [];
|
|
114498
|
+
}
|
|
114499
|
+
function defaultReasoningEffort(model) {
|
|
114500
|
+
return reasoningCapabilities(model)?.defaultEffort ?? null;
|
|
114501
|
+
}
|
|
114502
|
+
function requiresReasoning(model) {
|
|
114503
|
+
return reasoningCapabilities(model)?.mandatory === true;
|
|
114504
|
+
}
|
|
114505
|
+
function resolveReasoningEffort(model, requested) {
|
|
114506
|
+
const value = String(requested || "").trim().toLowerCase();
|
|
114507
|
+
if (!value) return null;
|
|
114508
|
+
const capabilities = reasoningCapabilities(model);
|
|
114509
|
+
if (!capabilities) return value;
|
|
114510
|
+
return capabilities.supportedEfforts.includes(value) ? value : null;
|
|
114511
|
+
}
|
|
114512
|
+
function reasoningOverrideFor(agent) {
|
|
114513
|
+
const effort = resolveReasoningEffort(agent?.model, agent?.reasoningEffort);
|
|
114514
|
+
return effort ? { effort } : { max_tokens: 256, exclude: false };
|
|
114515
|
+
}
|
|
114516
|
+
function truncationReasoningOverride(model) {
|
|
114517
|
+
const capabilities = reasoningCapabilities(model);
|
|
114518
|
+
if (!capabilities) return { effort: "low" };
|
|
114519
|
+
return capabilities.supportedEfforts.includes("low") ? { effort: "low" } : { max_tokens: 4096, exclude: false };
|
|
114520
|
+
}
|
|
114476
114521
|
function resolveTemperature(model, requested) {
|
|
114477
114522
|
if (modelSupportsParameter(model, "temperature") === false) return void 0;
|
|
114478
114523
|
return Number.isFinite(Number(requested)) ? Number(requested) : DEFAULT_TEMPERATURE;
|
|
@@ -114511,7 +114556,7 @@ function isReasoningModel(model) {
|
|
|
114511
114556
|
return /(?:^|[\/._:-])(qwen3|qwq|deepseek-[rv]\d|magistral|glm-(?:\d+[a-z]*|latest|flash-latest)|gpt-oss|nemotron|reason(?:ing)?|thinking|o1|o3|o4)(?:$|[\/._:-])/.test(id2);
|
|
114512
114557
|
}
|
|
114513
114558
|
function wantsReasoning(agent) {
|
|
114514
|
-
return isReasoningModel(agent?.model) || agent?.captureReasoning === true;
|
|
114559
|
+
return isReasoningModel(agent?.model) || agent?.captureReasoning === true || reasoningCapabilities(agent?.model)?.mandatory === true || Boolean(resolveReasoningEffort(agent?.model, agent?.reasoningEffort));
|
|
114515
114560
|
}
|
|
114516
114561
|
function effectiveProtocol(agent, connection) {
|
|
114517
114562
|
if (isOpenRouterConnection(connection) && isJevModel(agent?.model)) return "jev_decisions";
|
|
@@ -115287,7 +115332,7 @@ Respond with one of: ${criteriaKeys.map((k) => `${k} = ${criteria[k]}`).join(";
|
|
|
115287
115332
|
}
|
|
115288
115333
|
if (isOpenRouter) {
|
|
115289
115334
|
body.provider = agent.provider ? { only: [agent.provider], allow_fallbacks: false, require_parameters: true } : { allow_fallbacks: true, require_parameters: true };
|
|
115290
|
-
if (wantsReasoning(agent)) body.reasoning = reasoning ??
|
|
115335
|
+
if (wantsReasoning(agent)) body.reasoning = reasoning ?? reasoningOverrideFor(agent);
|
|
115291
115336
|
}
|
|
115292
115337
|
if (stream) {
|
|
115293
115338
|
body.stream = true;
|
|
@@ -115350,7 +115395,7 @@ async function decideOpenAICompatible({ agent, connection, state, legalActions,
|
|
|
115350
115395
|
result = await execute(actualProtocol);
|
|
115351
115396
|
} else if (err?.truncated) {
|
|
115352
115397
|
truncationRetry = true;
|
|
115353
|
-
result = await execute(actualProtocol, { maxTokens: 4096, reasoning:
|
|
115398
|
+
result = await execute(actualProtocol, { maxTokens: 4096, reasoning: truncationReasoningOverride(agent.model) });
|
|
115354
115399
|
} else throw err;
|
|
115355
115400
|
}
|
|
115356
115401
|
const normalized = normalizeDecisionObject(result.obj, legalActions, decisionId);
|
|
@@ -115526,7 +115571,7 @@ async function requestChatStage({ agent, connection, schema, toolName, systemPro
|
|
|
115526
115571
|
}
|
|
115527
115572
|
if (isOpenRouter) {
|
|
115528
115573
|
body.provider = agent.provider ? { only: [agent.provider], allow_fallbacks: false, require_parameters: true } : { allow_fallbacks: true, require_parameters: true };
|
|
115529
|
-
if (wantsReasoning(agent)) body.reasoning = override.reasoning ??
|
|
115574
|
+
if (wantsReasoning(agent)) body.reasoning = override.reasoning ?? reasoningOverrideFor(agent);
|
|
115530
115575
|
}
|
|
115531
115576
|
if (onDelta) {
|
|
115532
115577
|
body.stream = true;
|
|
@@ -115570,7 +115615,7 @@ async function requestChatStage({ agent, connection, schema, toolName, systemPro
|
|
|
115570
115615
|
return { ...result, protocolFallback: "tool\u2192json_schema" };
|
|
115571
115616
|
}
|
|
115572
115617
|
if (err?.truncated) {
|
|
115573
|
-
const result = await execute(actual, { maxTokens: 4096, reasoning:
|
|
115618
|
+
const result = await execute(actual, { maxTokens: 4096, reasoning: truncationReasoningOverride(agent.model) });
|
|
115574
115619
|
return { ...result, protocolFallback: "truncated\u2192retry" };
|
|
115575
115620
|
}
|
|
115576
115621
|
throw err;
|
|
@@ -115843,6 +115888,8 @@ var els = {
|
|
|
115843
115888
|
seatProtocol: $("#seatProtocol"),
|
|
115844
115889
|
seatProvider: $("#seatProvider"),
|
|
115845
115890
|
seatCaptureReasoning: $("#seatCaptureReasoning"),
|
|
115891
|
+
seatReasoningEffort: $("#seatReasoningEffort"),
|
|
115892
|
+
seatReasoningEffortHelp: $("#seatReasoningEffortHelp"),
|
|
115846
115893
|
seatError: $("#seatError"),
|
|
115847
115894
|
removeSeatBtn: $("#removeSeatBtn"),
|
|
115848
115895
|
cancelSeatBtn: $("#cancelSeatBtn"),
|
|
@@ -115893,6 +115940,7 @@ var WINNER_REVIEW_MS = 2e3;
|
|
|
115893
115940
|
var seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
|
|
115894
115941
|
var editingSeatIndex = null;
|
|
115895
115942
|
var seatNameAuto = false;
|
|
115943
|
+
var seatReasoningEffortDraft = "";
|
|
115896
115944
|
var lobbyVisible = true;
|
|
115897
115945
|
var pendingAutostart = false;
|
|
115898
115946
|
var arenaMaxDecisions = 0;
|
|
@@ -116198,7 +116246,8 @@ function injectedEnvironmentConfig() {
|
|
|
116198
116246
|
model: String(player.model || ""),
|
|
116199
116247
|
protocol: String(player.protocol || "tool"),
|
|
116200
116248
|
provider: String(player.provider || ""),
|
|
116201
|
-
captureReasoning: Boolean(player.captureReasoning)
|
|
116249
|
+
captureReasoning: Boolean(player.captureReasoning),
|
|
116250
|
+
reasoningEffort: String(player.reasoningEffort || "").trim().toLowerCase()
|
|
116202
116251
|
}))
|
|
116203
116252
|
};
|
|
116204
116253
|
}
|
|
@@ -116515,7 +116564,7 @@ function normalizeConfig(input = {}) {
|
|
|
116515
116564
|
if (!connIds.has(raw.connectionId)) throw new Error(`Connection missing for ${name}`);
|
|
116516
116565
|
const model = String(raw.model || "").trim();
|
|
116517
116566
|
if (!model) throw new Error(`Model missing for ${name}`);
|
|
116518
|
-
return { id: `player-${lobbySeat + 1}`, seat: index, lobbySeat, name, connectionId: raw.connectionId, model, protocol: raw.protocol || "tool", provider: String(raw.provider || "").trim(), captureReasoning: raw.captureReasoning === true, temperature: clamp(Number(raw.temperature) || 0.3, 0, 2) };
|
|
116567
|
+
return { id: `player-${lobbySeat + 1}`, seat: index, lobbySeat, name, connectionId: raw.connectionId, model, protocol: raw.protocol || "tool", provider: String(raw.provider || "").trim(), captureReasoning: raw.captureReasoning === true, reasoningEffort: String(raw.reasoningEffort || "").trim().toLowerCase(), temperature: clamp(Number(raw.temperature) || 0.3, 0, 2) };
|
|
116519
116568
|
})
|
|
116520
116569
|
};
|
|
116521
116570
|
}
|
|
@@ -116833,6 +116882,7 @@ var TournamentDirector = class {
|
|
|
116833
116882
|
model: agent.model,
|
|
116834
116883
|
connection: connection?.name ?? agent.connectionId,
|
|
116835
116884
|
provider: agent.provider || "auto",
|
|
116885
|
+
reasoningEffort: agent.reasoningEffort || null,
|
|
116836
116886
|
protocol: effectiveProtocol(agent, connection),
|
|
116837
116887
|
handNumber: this.handNumber,
|
|
116838
116888
|
street: this.engine.state.street,
|
|
@@ -116981,6 +117031,7 @@ var TournamentDirector = class {
|
|
|
116981
117031
|
configuredModel: agent.model,
|
|
116982
117032
|
resolvedModel: result?.model ?? agent.model,
|
|
116983
117033
|
provider: agent.provider || "auto",
|
|
117034
|
+
reasoningEffort: agent.reasoningEffort || null,
|
|
116984
117035
|
action: { id: chosen.id, type: chosen.type, amount: chosen.amount, description: chosen.description },
|
|
116985
117036
|
forced,
|
|
116986
117037
|
legalActions: legalActions.map((a) => ({ id: a.id, type: a.type, amount: a.amount ?? null, description: a.description })),
|
|
@@ -117221,6 +117272,7 @@ function renderModelOptions(models, currentValue = "") {
|
|
|
117221
117272
|
if (currentValue && !byId.has(currentValue)) byId.set(currentValue, { id: currentValue, name: currentValue });
|
|
117222
117273
|
const rows = [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
117223
117274
|
els.seatModelOptions.innerHTML = rows.map((model) => `<option value="${escapeHtml(model.id)}" label="${escapeHtml(model.name || model.id)}"></option>`).join("");
|
|
117275
|
+
updateSeatReasoningEffortOptions();
|
|
117224
117276
|
}
|
|
117225
117277
|
async function refreshSeatModelCatalog({ force = false } = {}) {
|
|
117226
117278
|
const connection = fullConnectionById(els.seatConnection?.value);
|
|
@@ -117274,9 +117326,41 @@ function defaultSeatDraft(seatIndex) {
|
|
|
117274
117326
|
model: conn?.kind === "typesafe" ? "jev-latest" : "",
|
|
117275
117327
|
protocol: conn?.kind === "typesafe" ? "jev_native" : "tool",
|
|
117276
117328
|
provider: "",
|
|
117277
|
-
captureReasoning: false
|
|
117329
|
+
captureReasoning: false,
|
|
117330
|
+
reasoningEffort: ""
|
|
117278
117331
|
};
|
|
117279
117332
|
}
|
|
117333
|
+
function updateSeatReasoningEffortOptions(selected) {
|
|
117334
|
+
const select = els.seatReasoningEffort;
|
|
117335
|
+
if (!select) return;
|
|
117336
|
+
const openRouter = isOpenRouterConnection(connectionById(els.seatConnection.value));
|
|
117337
|
+
const model = els.seatModel.value.trim();
|
|
117338
|
+
const efforts = openRouter ? reasoningEffortsFor(model) : [];
|
|
117339
|
+
const modelDefault = efforts.length ? defaultReasoningEffort(model) : null;
|
|
117340
|
+
const current = String(selected ?? seatReasoningEffortDraft ?? "").trim().toLowerCase();
|
|
117341
|
+
select.innerHTML = [
|
|
117342
|
+
'<option value="">Arena default (capped)</option>',
|
|
117343
|
+
...efforts.map((effort) => `<option value="${escapeHtml(effort)}">${escapeHtml(effort)}${effort === modelDefault ? " \xB7 model default" : ""}</option>`)
|
|
117344
|
+
].join("");
|
|
117345
|
+
if (efforts.includes(current)) {
|
|
117346
|
+
select.value = current;
|
|
117347
|
+
seatReasoningEffortDraft = current;
|
|
117348
|
+
} else {
|
|
117349
|
+
select.value = "";
|
|
117350
|
+
if (!current) seatReasoningEffortDraft = "";
|
|
117351
|
+
}
|
|
117352
|
+
select.disabled = !efforts.length;
|
|
117353
|
+
const help = els.seatReasoningEffortHelp;
|
|
117354
|
+
if (!help) return;
|
|
117355
|
+
help.dataset.tone = "";
|
|
117356
|
+
if (!openRouter) help.textContent = "Reasoning effort is available for OpenRouter models that advertise it.";
|
|
117357
|
+
else if (!efforts.length) help.textContent = "This model does not advertise reasoning efforts; the capped default is used.";
|
|
117358
|
+
else if (!select.value && requiresReasoning(model)) {
|
|
117359
|
+
help.textContent = `This model always reasons and ignores the capped default, so it can exceed the action clock and auto-fold. Pick an effort \u2014 ${efforts.includes("minimal") ? "minimal" : efforts[0]} is usually fastest.`;
|
|
117360
|
+
help.dataset.tone = "bad";
|
|
117361
|
+
} else if (modelDefault) help.textContent = `Model default is ${modelDefault}. Lower it when a slow reasoner runs out of action clock and auto-folds.`;
|
|
117362
|
+
else help.textContent = "Lower it when a slow reasoner runs out of action clock and auto-folds.";
|
|
117363
|
+
}
|
|
117280
117364
|
function applySeatProtocolRules() {
|
|
117281
117365
|
const conn = connectionById(els.seatConnection.value);
|
|
117282
117366
|
const kind = conn?.kind || "openrouter";
|
|
@@ -117285,6 +117369,7 @@ function applySeatProtocolRules() {
|
|
|
117285
117369
|
const jevSeat = kind === "typesafe" || jevViaOpenRouter;
|
|
117286
117370
|
document.querySelector(".seat-provider-field")?.classList.toggle("hidden", !openRouter || jevViaOpenRouter);
|
|
117287
117371
|
document.querySelector(".seat-reasoning-field")?.classList.toggle("hidden", jevSeat);
|
|
117372
|
+
document.querySelector(".seat-effort-field")?.classList.toggle("hidden", jevSeat);
|
|
117288
117373
|
if (els.seatCaptureReasoning) {
|
|
117289
117374
|
els.seatCaptureReasoning.disabled = jevSeat;
|
|
117290
117375
|
const alreadyReasoning = isReasoningModel(els.seatModel.value.trim());
|
|
@@ -117305,6 +117390,7 @@ function applySeatProtocolRules() {
|
|
|
117305
117390
|
els.seatProvider.disabled = !openRouter;
|
|
117306
117391
|
if (els.seatModel.value === "jev-latest") els.seatModel.value = "";
|
|
117307
117392
|
}
|
|
117393
|
+
updateSeatReasoningEffortOptions();
|
|
117308
117394
|
}
|
|
117309
117395
|
function updateSeatSummary() {
|
|
117310
117396
|
const count = seatAssignments.filter(Boolean).length;
|
|
@@ -117331,13 +117417,16 @@ function openSeatEditor(seatIndex) {
|
|
|
117331
117417
|
els.seatProtocol.value = draft.protocol || "tool";
|
|
117332
117418
|
els.seatProvider.value = draft.provider || "";
|
|
117333
117419
|
if (els.seatCaptureReasoning) els.seatCaptureReasoning.checked = Boolean(draft.captureReasoning);
|
|
117420
|
+
seatReasoningEffortDraft = String(draft.reasoningEffort || "").trim().toLowerCase();
|
|
117334
117421
|
applySeatProtocolRules();
|
|
117422
|
+
updateSeatReasoningEffortOptions(seatReasoningEffortDraft);
|
|
117335
117423
|
syncSeatNameFromModel();
|
|
117336
117424
|
void refreshSeatModelCatalog();
|
|
117337
117425
|
els.seatError.classList.add("hidden");
|
|
117338
117426
|
els.seatLockNotice.classList.toggle("hidden", !locked);
|
|
117339
117427
|
for (const control of [els.seatName, els.seatConnection, els.seatModel, els.seatProtocol, els.seatProvider]) control.disabled = locked || control === els.seatProvider && (!isOpenRouterConnection(connectionById(els.seatConnection.value)) || isJevModel(els.seatModel.value));
|
|
117340
117428
|
if (els.seatCaptureReasoning) els.seatCaptureReasoning.disabled = locked || els.seatCaptureReasoning.disabled;
|
|
117429
|
+
if (els.seatReasoningEffort) els.seatReasoningEffort.disabled = locked || els.seatReasoningEffort.disabled;
|
|
117341
117430
|
els.saveSeatBtn.disabled = locked;
|
|
117342
117431
|
els.removeSeatBtn.disabled = locked || !seatAssignments[seatIndex];
|
|
117343
117432
|
if (!els.seatDialog.open) els.seatDialog.showModal();
|
|
@@ -117355,7 +117444,8 @@ function readSeatDraft() {
|
|
|
117355
117444
|
model: els.seatModel.value.trim(),
|
|
117356
117445
|
protocol: els.seatProtocol.value,
|
|
117357
117446
|
provider: els.seatProvider.disabled ? "" : els.seatProvider.value.trim(),
|
|
117358
|
-
captureReasoning: Boolean(els.seatCaptureReasoning && !els.seatCaptureReasoning.disabled && els.seatCaptureReasoning.checked)
|
|
117447
|
+
captureReasoning: Boolean(els.seatCaptureReasoning && !els.seatCaptureReasoning.disabled && els.seatCaptureReasoning.checked),
|
|
117448
|
+
reasoningEffort: els.seatReasoningEffort && !els.seatReasoningEffort.disabled ? els.seatReasoningEffort.value.trim().toLowerCase() : ""
|
|
117359
117449
|
};
|
|
117360
117450
|
}
|
|
117361
117451
|
function readSeatPlayers() {
|
|
@@ -117384,7 +117474,8 @@ function restoreSeatAssignments(rows = []) {
|
|
|
117384
117474
|
model: player.model || "",
|
|
117385
117475
|
protocol: player.protocol || "tool",
|
|
117386
117476
|
provider: player.provider || "",
|
|
117387
|
-
captureReasoning: player.captureReasoning === true
|
|
117477
|
+
captureReasoning: player.captureReasoning === true,
|
|
117478
|
+
reasoningEffort: String(player.reasoningEffort || "").trim().toLowerCase()
|
|
117388
117479
|
};
|
|
117389
117480
|
});
|
|
117390
117481
|
updateSeatSummary();
|
|
@@ -118116,7 +118207,7 @@ function renderStats(s) {
|
|
|
118116
118207
|
function tableRenderSignature(s) {
|
|
118117
118208
|
const running = ["RUNNING", "PAUSED"].includes(s?.status);
|
|
118118
118209
|
if (lobbyVisible && !running) {
|
|
118119
|
-
return JSON.stringify(["lobby", seatAssignments.map((p) => p ? [p.name, p.model, p.protocol, p.connectionId, p.provider, Boolean(p.captureReasoning)] : null)]);
|
|
118210
|
+
return JSON.stringify(["lobby", seatAssignments.map((p) => p ? [p.name, p.model, p.protocol, p.connectionId, p.provider, Boolean(p.captureReasoning), p.reasoningEffort || ""] : null)]);
|
|
118120
118211
|
}
|
|
118121
118212
|
const t = s?.table;
|
|
118122
118213
|
if (!t) return JSON.stringify(["empty", s?.status || "IDLE"]);
|
|
@@ -119500,6 +119591,9 @@ els.seatName.addEventListener("input", () => {
|
|
|
119500
119591
|
});
|
|
119501
119592
|
els.seatProtocol.addEventListener("change", applySeatProtocolRules);
|
|
119502
119593
|
els.seatCaptureReasoning?.addEventListener("change", applySeatProtocolRules);
|
|
119594
|
+
els.seatReasoningEffort?.addEventListener("change", () => {
|
|
119595
|
+
seatReasoningEffortDraft = els.seatReasoningEffort.value;
|
|
119596
|
+
});
|
|
119503
119597
|
els.refreshModelsBtn?.addEventListener("click", () => void refreshSeatModelCatalog({ force: true }));
|
|
119504
119598
|
els.removeSeatBtn.addEventListener("click", () => {
|
|
119505
119599
|
if (editingSeatIndex == null || director && ["RUNNING", "PAUSED"].includes(director.status)) return;
|
package/dist/index.html
CHANGED
|
@@ -304,6 +304,11 @@
|
|
|
304
304
|
<span class="checkbox-field"><input id="seatCaptureReasoning" type="checkbox" /> Capture raw model reasoning</span>
|
|
305
305
|
<span class="field-help seat-reasoning-help hidden">This model is not a reasoning model by default. Capturing its reasoning can be far slower (Gemma measured ~2s → 27–48s per decision) and is outside the standard benchmark track. Raise the action clock before using it.</span>
|
|
306
306
|
</label>
|
|
307
|
+
<label class="seat-effort-field">
|
|
308
|
+
<span class="field-label-row">Reasoning effort <button class="info-button inline-info" type="button" aria-label="About reasoning effort" data-tip="How much hidden thinking the model may spend. Higher effort is smarter but slower; a slow reasoner can exhaust the action clock and be forced to fold. Options come from the OpenRouter model catalogue, and an unset value keeps the arena's fixed reasoning cap.">i</button></span>
|
|
309
|
+
<select id="seatReasoningEffort"></select>
|
|
310
|
+
<span id="seatReasoningEffortHelp" class="field-help"></span>
|
|
311
|
+
</label>
|
|
307
312
|
</div>
|
|
308
313
|
</div>
|
|
309
314
|
<div id="seatError" class="setup-error hidden"></div>
|
|
@@ -548,7 +548,7 @@ label input, label select { margin-top: 5px; font-weight: 450; }
|
|
|
548
548
|
.split-footer { justify-content: space-between; }
|
|
549
549
|
.seat-footer-actions, .replay-share-actions { display: flex; gap: 7px; }
|
|
550
550
|
.seat-editor-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
|
551
|
-
.seat-model-field { grid-column: 1 / -1; }
|
|
551
|
+
.seat-model-field, .seat-effort-field { grid-column: 1 / -1; }
|
|
552
552
|
.model-input-wrap { margin-top: 6px; display: grid; grid-template-columns: minmax(0, 1fr) 38px; gap: 8px; }
|
|
553
553
|
.model-input-wrap input { margin: 0; }
|
|
554
554
|
.field-icon-button, .dialog-body .remove-connection { width: 38px; height: 38px; min-height: 38px; border-radius: 8px; }
|
|
@@ -1041,6 +1041,11 @@ label input, label select { margin-top: 5px; font-weight: 450; }
|
|
|
1041
1041
|
<span class="checkbox-field"><input id="seatCaptureReasoning" type="checkbox" /> Capture raw model reasoning</span>
|
|
1042
1042
|
<span class="field-help seat-reasoning-help hidden">This model is not a reasoning model by default. Capturing its reasoning can be far slower (Gemma measured ~2s → 27–48s per decision) and is outside the standard benchmark track. Raise the action clock before using it.</span>
|
|
1043
1043
|
</label>
|
|
1044
|
+
<label class="seat-effort-field">
|
|
1045
|
+
<span class="field-label-row">Reasoning effort <button class="info-button inline-info" type="button" aria-label="About reasoning effort" data-tip="How much hidden thinking the model may spend. Higher effort is smarter but slower; a slow reasoner can exhaust the action clock and be forced to fold. Options come from the OpenRouter model catalogue, and an unset value keeps the arena's fixed reasoning cap.">i</button></span>
|
|
1046
|
+
<select id="seatReasoningEffort"></select>
|
|
1047
|
+
<span id="seatReasoningEffortHelp" class="field-help"></span>
|
|
1048
|
+
</label>
|
|
1044
1049
|
</div>
|
|
1045
1050
|
</div>
|
|
1046
1051
|
<div id="seatError" class="setup-error hidden"></div>
|
|
@@ -115591,8 +115596,15 @@ async function fetchStreamingJson(url, options, { maxRetries = 1, onDelta = null
|
|
|
115591
115596
|
throw new ArenaRequestError(message, { status: response.status, category: requestErrorCategory(response.status), payload, incidents });
|
|
115592
115597
|
}
|
|
115593
115598
|
}
|
|
115599
|
+
function requestErrorDetail(err) {
|
|
115600
|
+
const parts = [summarizeError(err)];
|
|
115601
|
+
const nested = err?.payload?.error?.metadata?.raw ?? err?.payload?.metadata?.raw;
|
|
115602
|
+
if (typeof nested === "string") parts.push(nested);
|
|
115603
|
+
else if (nested) parts.push(String(JSON.stringify(nested)));
|
|
115604
|
+
return parts.join(" ");
|
|
115605
|
+
}
|
|
115594
115606
|
function isUnsupportedToolChoiceError(err) {
|
|
115595
|
-
const message =
|
|
115607
|
+
const message = requestErrorDetail(err);
|
|
115596
115608
|
return [400, 404, 422].includes(Number(err?.status)) && /tool[_ -]?choice|tool call|tools?.*(?:unsupported|support)|no endpoints?.*support/i.test(message);
|
|
115597
115609
|
}
|
|
115598
115610
|
function mapGet(mapish, key, fallback = 0) {
|
|
@@ -115618,13 +115630,23 @@ function jsonSafe(value) {
|
|
|
115618
115630
|
}
|
|
115619
115631
|
var protocolCapabilityCache = /* @__PURE__ */ new Map();
|
|
115620
115632
|
var modelSupportedParameters = /* @__PURE__ */ new Map();
|
|
115633
|
+
var modelReasoningCapabilities = /* @__PURE__ */ new Map();
|
|
115621
115634
|
var DEFAULT_TEMPERATURE = 0.3;
|
|
115622
115635
|
function registerModelCapabilities(models = []) {
|
|
115623
115636
|
for (const entry of models ?? []) {
|
|
115624
115637
|
const id2 = typeof entry === "string" ? entry : entry?.id;
|
|
115638
|
+
if (!id2) continue;
|
|
115625
115639
|
const parameters = typeof entry === "object" && entry ? entry.supported_parameters : null;
|
|
115626
|
-
if (
|
|
115627
|
-
|
|
115640
|
+
if (Array.isArray(parameters)) modelSupportedParameters.set(String(id2), new Set(parameters.map(String)));
|
|
115641
|
+
const reasoning = typeof entry === "object" && entry ? entry.reasoning : null;
|
|
115642
|
+
if (!reasoning || typeof reasoning !== "object") continue;
|
|
115643
|
+
const efforts = Array.isArray(reasoning.supported_efforts) ? reasoning.supported_efforts.map((value) => String(value).trim().toLowerCase()).filter(Boolean) : [];
|
|
115644
|
+
const declaredDefault = String(reasoning.default_effort || "").trim().toLowerCase();
|
|
115645
|
+
modelReasoningCapabilities.set(String(id2), {
|
|
115646
|
+
mandatory: reasoning.mandatory === true,
|
|
115647
|
+
supportedEfforts: efforts,
|
|
115648
|
+
defaultEffort: efforts.includes(declaredDefault) ? declaredDefault : null
|
|
115649
|
+
});
|
|
115628
115650
|
}
|
|
115629
115651
|
}
|
|
115630
115652
|
function modelSupportsParameter(model, parameter) {
|
|
@@ -115632,6 +115654,34 @@ function modelSupportsParameter(model, parameter) {
|
|
|
115632
115654
|
if (!supported) return null;
|
|
115633
115655
|
return supported.has(String(parameter));
|
|
115634
115656
|
}
|
|
115657
|
+
function reasoningCapabilities(model) {
|
|
115658
|
+
return modelReasoningCapabilities.get(String(model || "")) ?? null;
|
|
115659
|
+
}
|
|
115660
|
+
function reasoningEffortsFor(model) {
|
|
115661
|
+
return reasoningCapabilities(model)?.supportedEfforts ?? [];
|
|
115662
|
+
}
|
|
115663
|
+
function defaultReasoningEffort(model) {
|
|
115664
|
+
return reasoningCapabilities(model)?.defaultEffort ?? null;
|
|
115665
|
+
}
|
|
115666
|
+
function requiresReasoning(model) {
|
|
115667
|
+
return reasoningCapabilities(model)?.mandatory === true;
|
|
115668
|
+
}
|
|
115669
|
+
function resolveReasoningEffort(model, requested) {
|
|
115670
|
+
const value = String(requested || "").trim().toLowerCase();
|
|
115671
|
+
if (!value) return null;
|
|
115672
|
+
const capabilities = reasoningCapabilities(model);
|
|
115673
|
+
if (!capabilities) return value;
|
|
115674
|
+
return capabilities.supportedEfforts.includes(value) ? value : null;
|
|
115675
|
+
}
|
|
115676
|
+
function reasoningOverrideFor(agent) {
|
|
115677
|
+
const effort = resolveReasoningEffort(agent?.model, agent?.reasoningEffort);
|
|
115678
|
+
return effort ? { effort } : { max_tokens: 256, exclude: false };
|
|
115679
|
+
}
|
|
115680
|
+
function truncationReasoningOverride(model) {
|
|
115681
|
+
const capabilities = reasoningCapabilities(model);
|
|
115682
|
+
if (!capabilities) return { effort: "low" };
|
|
115683
|
+
return capabilities.supportedEfforts.includes("low") ? { effort: "low" } : { max_tokens: 4096, exclude: false };
|
|
115684
|
+
}
|
|
115635
115685
|
function resolveTemperature(model, requested) {
|
|
115636
115686
|
if (modelSupportsParameter(model, "temperature") === false) return void 0;
|
|
115637
115687
|
return Number.isFinite(Number(requested)) ? Number(requested) : DEFAULT_TEMPERATURE;
|
|
@@ -115670,7 +115720,7 @@ function isReasoningModel(model) {
|
|
|
115670
115720
|
return /(?:^|[\/._:-])(qwen3|qwq|deepseek-[rv]\d|magistral|glm-(?:\d+[a-z]*|latest|flash-latest)|gpt-oss|nemotron|reason(?:ing)?|thinking|o1|o3|o4)(?:$|[\/._:-])/.test(id2);
|
|
115671
115721
|
}
|
|
115672
115722
|
function wantsReasoning(agent) {
|
|
115673
|
-
return isReasoningModel(agent?.model) || agent?.captureReasoning === true;
|
|
115723
|
+
return isReasoningModel(agent?.model) || agent?.captureReasoning === true || reasoningCapabilities(agent?.model)?.mandatory === true || Boolean(resolveReasoningEffort(agent?.model, agent?.reasoningEffort));
|
|
115674
115724
|
}
|
|
115675
115725
|
function effectiveProtocol(agent, connection) {
|
|
115676
115726
|
if (isOpenRouterConnection(connection) && isJevModel(agent?.model)) return "jev_decisions";
|
|
@@ -116446,7 +116496,7 @@ Respond with one of: ${criteriaKeys.map((k) => `${k} = ${criteria[k]}`).join(";
|
|
|
116446
116496
|
}
|
|
116447
116497
|
if (isOpenRouter) {
|
|
116448
116498
|
body.provider = agent.provider ? { only: [agent.provider], allow_fallbacks: false, require_parameters: true } : { allow_fallbacks: true, require_parameters: true };
|
|
116449
|
-
if (wantsReasoning(agent)) body.reasoning = reasoning ??
|
|
116499
|
+
if (wantsReasoning(agent)) body.reasoning = reasoning ?? reasoningOverrideFor(agent);
|
|
116450
116500
|
}
|
|
116451
116501
|
if (stream) {
|
|
116452
116502
|
body.stream = true;
|
|
@@ -116509,7 +116559,7 @@ async function decideOpenAICompatible({ agent, connection, state, legalActions,
|
|
|
116509
116559
|
result = await execute(actualProtocol);
|
|
116510
116560
|
} else if (err?.truncated) {
|
|
116511
116561
|
truncationRetry = true;
|
|
116512
|
-
result = await execute(actualProtocol, { maxTokens: 4096, reasoning:
|
|
116562
|
+
result = await execute(actualProtocol, { maxTokens: 4096, reasoning: truncationReasoningOverride(agent.model) });
|
|
116513
116563
|
} else throw err;
|
|
116514
116564
|
}
|
|
116515
116565
|
const normalized = normalizeDecisionObject(result.obj, legalActions, decisionId);
|
|
@@ -116685,7 +116735,7 @@ async function requestChatStage({ agent, connection, schema, toolName, systemPro
|
|
|
116685
116735
|
}
|
|
116686
116736
|
if (isOpenRouter) {
|
|
116687
116737
|
body.provider = agent.provider ? { only: [agent.provider], allow_fallbacks: false, require_parameters: true } : { allow_fallbacks: true, require_parameters: true };
|
|
116688
|
-
if (wantsReasoning(agent)) body.reasoning = override.reasoning ??
|
|
116738
|
+
if (wantsReasoning(agent)) body.reasoning = override.reasoning ?? reasoningOverrideFor(agent);
|
|
116689
116739
|
}
|
|
116690
116740
|
if (onDelta) {
|
|
116691
116741
|
body.stream = true;
|
|
@@ -116729,7 +116779,7 @@ async function requestChatStage({ agent, connection, schema, toolName, systemPro
|
|
|
116729
116779
|
return { ...result, protocolFallback: "tool\u2192json_schema" };
|
|
116730
116780
|
}
|
|
116731
116781
|
if (err?.truncated) {
|
|
116732
|
-
const result = await execute(actual, { maxTokens: 4096, reasoning:
|
|
116782
|
+
const result = await execute(actual, { maxTokens: 4096, reasoning: truncationReasoningOverride(agent.model) });
|
|
116733
116783
|
return { ...result, protocolFallback: "truncated\u2192retry" };
|
|
116734
116784
|
}
|
|
116735
116785
|
throw err;
|
|
@@ -117002,6 +117052,8 @@ var els = {
|
|
|
117002
117052
|
seatProtocol: $("#seatProtocol"),
|
|
117003
117053
|
seatProvider: $("#seatProvider"),
|
|
117004
117054
|
seatCaptureReasoning: $("#seatCaptureReasoning"),
|
|
117055
|
+
seatReasoningEffort: $("#seatReasoningEffort"),
|
|
117056
|
+
seatReasoningEffortHelp: $("#seatReasoningEffortHelp"),
|
|
117005
117057
|
seatError: $("#seatError"),
|
|
117006
117058
|
removeSeatBtn: $("#removeSeatBtn"),
|
|
117007
117059
|
cancelSeatBtn: $("#cancelSeatBtn"),
|
|
@@ -117052,6 +117104,7 @@ var WINNER_REVIEW_MS = 2e3;
|
|
|
117052
117104
|
var seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
|
|
117053
117105
|
var editingSeatIndex = null;
|
|
117054
117106
|
var seatNameAuto = false;
|
|
117107
|
+
var seatReasoningEffortDraft = "";
|
|
117055
117108
|
var lobbyVisible = true;
|
|
117056
117109
|
var pendingAutostart = false;
|
|
117057
117110
|
var arenaMaxDecisions = 0;
|
|
@@ -117357,7 +117410,8 @@ function injectedEnvironmentConfig() {
|
|
|
117357
117410
|
model: String(player.model || ""),
|
|
117358
117411
|
protocol: String(player.protocol || "tool"),
|
|
117359
117412
|
provider: String(player.provider || ""),
|
|
117360
|
-
captureReasoning: Boolean(player.captureReasoning)
|
|
117413
|
+
captureReasoning: Boolean(player.captureReasoning),
|
|
117414
|
+
reasoningEffort: String(player.reasoningEffort || "").trim().toLowerCase()
|
|
117361
117415
|
}))
|
|
117362
117416
|
};
|
|
117363
117417
|
}
|
|
@@ -117674,7 +117728,7 @@ function normalizeConfig(input = {}) {
|
|
|
117674
117728
|
if (!connIds.has(raw.connectionId)) throw new Error(`Connection missing for ${name}`);
|
|
117675
117729
|
const model = String(raw.model || "").trim();
|
|
117676
117730
|
if (!model) throw new Error(`Model missing for ${name}`);
|
|
117677
|
-
return { id: `player-${lobbySeat + 1}`, seat: index, lobbySeat, name, connectionId: raw.connectionId, model, protocol: raw.protocol || "tool", provider: String(raw.provider || "").trim(), captureReasoning: raw.captureReasoning === true, temperature: clamp(Number(raw.temperature) || 0.3, 0, 2) };
|
|
117731
|
+
return { id: `player-${lobbySeat + 1}`, seat: index, lobbySeat, name, connectionId: raw.connectionId, model, protocol: raw.protocol || "tool", provider: String(raw.provider || "").trim(), captureReasoning: raw.captureReasoning === true, reasoningEffort: String(raw.reasoningEffort || "").trim().toLowerCase(), temperature: clamp(Number(raw.temperature) || 0.3, 0, 2) };
|
|
117678
117732
|
})
|
|
117679
117733
|
};
|
|
117680
117734
|
}
|
|
@@ -117992,6 +118046,7 @@ var TournamentDirector = class {
|
|
|
117992
118046
|
model: agent.model,
|
|
117993
118047
|
connection: connection?.name ?? agent.connectionId,
|
|
117994
118048
|
provider: agent.provider || "auto",
|
|
118049
|
+
reasoningEffort: agent.reasoningEffort || null,
|
|
117995
118050
|
protocol: effectiveProtocol(agent, connection),
|
|
117996
118051
|
handNumber: this.handNumber,
|
|
117997
118052
|
street: this.engine.state.street,
|
|
@@ -118140,6 +118195,7 @@ var TournamentDirector = class {
|
|
|
118140
118195
|
configuredModel: agent.model,
|
|
118141
118196
|
resolvedModel: result?.model ?? agent.model,
|
|
118142
118197
|
provider: agent.provider || "auto",
|
|
118198
|
+
reasoningEffort: agent.reasoningEffort || null,
|
|
118143
118199
|
action: { id: chosen.id, type: chosen.type, amount: chosen.amount, description: chosen.description },
|
|
118144
118200
|
forced,
|
|
118145
118201
|
legalActions: legalActions.map((a) => ({ id: a.id, type: a.type, amount: a.amount ?? null, description: a.description })),
|
|
@@ -118380,6 +118436,7 @@ function renderModelOptions(models, currentValue = "") {
|
|
|
118380
118436
|
if (currentValue && !byId.has(currentValue)) byId.set(currentValue, { id: currentValue, name: currentValue });
|
|
118381
118437
|
const rows = [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
118382
118438
|
els.seatModelOptions.innerHTML = rows.map((model) => `<option value="${escapeHtml(model.id)}" label="${escapeHtml(model.name || model.id)}"></option>`).join("");
|
|
118439
|
+
updateSeatReasoningEffortOptions();
|
|
118383
118440
|
}
|
|
118384
118441
|
async function refreshSeatModelCatalog({ force = false } = {}) {
|
|
118385
118442
|
const connection = fullConnectionById(els.seatConnection?.value);
|
|
@@ -118433,9 +118490,41 @@ function defaultSeatDraft(seatIndex) {
|
|
|
118433
118490
|
model: conn?.kind === "typesafe" ? "jev-latest" : "",
|
|
118434
118491
|
protocol: conn?.kind === "typesafe" ? "jev_native" : "tool",
|
|
118435
118492
|
provider: "",
|
|
118436
|
-
captureReasoning: false
|
|
118493
|
+
captureReasoning: false,
|
|
118494
|
+
reasoningEffort: ""
|
|
118437
118495
|
};
|
|
118438
118496
|
}
|
|
118497
|
+
function updateSeatReasoningEffortOptions(selected) {
|
|
118498
|
+
const select = els.seatReasoningEffort;
|
|
118499
|
+
if (!select) return;
|
|
118500
|
+
const openRouter = isOpenRouterConnection(connectionById(els.seatConnection.value));
|
|
118501
|
+
const model = els.seatModel.value.trim();
|
|
118502
|
+
const efforts = openRouter ? reasoningEffortsFor(model) : [];
|
|
118503
|
+
const modelDefault = efforts.length ? defaultReasoningEffort(model) : null;
|
|
118504
|
+
const current = String(selected ?? seatReasoningEffortDraft ?? "").trim().toLowerCase();
|
|
118505
|
+
select.innerHTML = [
|
|
118506
|
+
'<option value="">Arena default (capped)</option>',
|
|
118507
|
+
...efforts.map((effort) => `<option value="${escapeHtml(effort)}">${escapeHtml(effort)}${effort === modelDefault ? " \xB7 model default" : ""}</option>`)
|
|
118508
|
+
].join("");
|
|
118509
|
+
if (efforts.includes(current)) {
|
|
118510
|
+
select.value = current;
|
|
118511
|
+
seatReasoningEffortDraft = current;
|
|
118512
|
+
} else {
|
|
118513
|
+
select.value = "";
|
|
118514
|
+
if (!current) seatReasoningEffortDraft = "";
|
|
118515
|
+
}
|
|
118516
|
+
select.disabled = !efforts.length;
|
|
118517
|
+
const help = els.seatReasoningEffortHelp;
|
|
118518
|
+
if (!help) return;
|
|
118519
|
+
help.dataset.tone = "";
|
|
118520
|
+
if (!openRouter) help.textContent = "Reasoning effort is available for OpenRouter models that advertise it.";
|
|
118521
|
+
else if (!efforts.length) help.textContent = "This model does not advertise reasoning efforts; the capped default is used.";
|
|
118522
|
+
else if (!select.value && requiresReasoning(model)) {
|
|
118523
|
+
help.textContent = `This model always reasons and ignores the capped default, so it can exceed the action clock and auto-fold. Pick an effort \u2014 ${efforts.includes("minimal") ? "minimal" : efforts[0]} is usually fastest.`;
|
|
118524
|
+
help.dataset.tone = "bad";
|
|
118525
|
+
} else if (modelDefault) help.textContent = `Model default is ${modelDefault}. Lower it when a slow reasoner runs out of action clock and auto-folds.`;
|
|
118526
|
+
else help.textContent = "Lower it when a slow reasoner runs out of action clock and auto-folds.";
|
|
118527
|
+
}
|
|
118439
118528
|
function applySeatProtocolRules() {
|
|
118440
118529
|
const conn = connectionById(els.seatConnection.value);
|
|
118441
118530
|
const kind = conn?.kind || "openrouter";
|
|
@@ -118444,6 +118533,7 @@ function applySeatProtocolRules() {
|
|
|
118444
118533
|
const jevSeat = kind === "typesafe" || jevViaOpenRouter;
|
|
118445
118534
|
document.querySelector(".seat-provider-field")?.classList.toggle("hidden", !openRouter || jevViaOpenRouter);
|
|
118446
118535
|
document.querySelector(".seat-reasoning-field")?.classList.toggle("hidden", jevSeat);
|
|
118536
|
+
document.querySelector(".seat-effort-field")?.classList.toggle("hidden", jevSeat);
|
|
118447
118537
|
if (els.seatCaptureReasoning) {
|
|
118448
118538
|
els.seatCaptureReasoning.disabled = jevSeat;
|
|
118449
118539
|
const alreadyReasoning = isReasoningModel(els.seatModel.value.trim());
|
|
@@ -118464,6 +118554,7 @@ function applySeatProtocolRules() {
|
|
|
118464
118554
|
els.seatProvider.disabled = !openRouter;
|
|
118465
118555
|
if (els.seatModel.value === "jev-latest") els.seatModel.value = "";
|
|
118466
118556
|
}
|
|
118557
|
+
updateSeatReasoningEffortOptions();
|
|
118467
118558
|
}
|
|
118468
118559
|
function updateSeatSummary() {
|
|
118469
118560
|
const count = seatAssignments.filter(Boolean).length;
|
|
@@ -118490,13 +118581,16 @@ function openSeatEditor(seatIndex) {
|
|
|
118490
118581
|
els.seatProtocol.value = draft.protocol || "tool";
|
|
118491
118582
|
els.seatProvider.value = draft.provider || "";
|
|
118492
118583
|
if (els.seatCaptureReasoning) els.seatCaptureReasoning.checked = Boolean(draft.captureReasoning);
|
|
118584
|
+
seatReasoningEffortDraft = String(draft.reasoningEffort || "").trim().toLowerCase();
|
|
118493
118585
|
applySeatProtocolRules();
|
|
118586
|
+
updateSeatReasoningEffortOptions(seatReasoningEffortDraft);
|
|
118494
118587
|
syncSeatNameFromModel();
|
|
118495
118588
|
void refreshSeatModelCatalog();
|
|
118496
118589
|
els.seatError.classList.add("hidden");
|
|
118497
118590
|
els.seatLockNotice.classList.toggle("hidden", !locked);
|
|
118498
118591
|
for (const control of [els.seatName, els.seatConnection, els.seatModel, els.seatProtocol, els.seatProvider]) control.disabled = locked || control === els.seatProvider && (!isOpenRouterConnection(connectionById(els.seatConnection.value)) || isJevModel(els.seatModel.value));
|
|
118499
118592
|
if (els.seatCaptureReasoning) els.seatCaptureReasoning.disabled = locked || els.seatCaptureReasoning.disabled;
|
|
118593
|
+
if (els.seatReasoningEffort) els.seatReasoningEffort.disabled = locked || els.seatReasoningEffort.disabled;
|
|
118500
118594
|
els.saveSeatBtn.disabled = locked;
|
|
118501
118595
|
els.removeSeatBtn.disabled = locked || !seatAssignments[seatIndex];
|
|
118502
118596
|
if (!els.seatDialog.open) els.seatDialog.showModal();
|
|
@@ -118514,7 +118608,8 @@ function readSeatDraft() {
|
|
|
118514
118608
|
model: els.seatModel.value.trim(),
|
|
118515
118609
|
protocol: els.seatProtocol.value,
|
|
118516
118610
|
provider: els.seatProvider.disabled ? "" : els.seatProvider.value.trim(),
|
|
118517
|
-
captureReasoning: Boolean(els.seatCaptureReasoning && !els.seatCaptureReasoning.disabled && els.seatCaptureReasoning.checked)
|
|
118611
|
+
captureReasoning: Boolean(els.seatCaptureReasoning && !els.seatCaptureReasoning.disabled && els.seatCaptureReasoning.checked),
|
|
118612
|
+
reasoningEffort: els.seatReasoningEffort && !els.seatReasoningEffort.disabled ? els.seatReasoningEffort.value.trim().toLowerCase() : ""
|
|
118518
118613
|
};
|
|
118519
118614
|
}
|
|
118520
118615
|
function readSeatPlayers() {
|
|
@@ -118543,7 +118638,8 @@ function restoreSeatAssignments(rows = []) {
|
|
|
118543
118638
|
model: player.model || "",
|
|
118544
118639
|
protocol: player.protocol || "tool",
|
|
118545
118640
|
provider: player.provider || "",
|
|
118546
|
-
captureReasoning: player.captureReasoning === true
|
|
118641
|
+
captureReasoning: player.captureReasoning === true,
|
|
118642
|
+
reasoningEffort: String(player.reasoningEffort || "").trim().toLowerCase()
|
|
118547
118643
|
};
|
|
118548
118644
|
});
|
|
118549
118645
|
updateSeatSummary();
|
|
@@ -119275,7 +119371,7 @@ function renderStats(s) {
|
|
|
119275
119371
|
function tableRenderSignature(s) {
|
|
119276
119372
|
const running = ["RUNNING", "PAUSED"].includes(s?.status);
|
|
119277
119373
|
if (lobbyVisible && !running) {
|
|
119278
|
-
return JSON.stringify(["lobby", seatAssignments.map((p) => p ? [p.name, p.model, p.protocol, p.connectionId, p.provider, Boolean(p.captureReasoning)] : null)]);
|
|
119374
|
+
return JSON.stringify(["lobby", seatAssignments.map((p) => p ? [p.name, p.model, p.protocol, p.connectionId, p.provider, Boolean(p.captureReasoning), p.reasoningEffort || ""] : null)]);
|
|
119279
119375
|
}
|
|
119280
119376
|
const t = s?.table;
|
|
119281
119377
|
if (!t) return JSON.stringify(["empty", s?.status || "IDLE"]);
|
|
@@ -120659,6 +120755,9 @@ els.seatName.addEventListener("input", () => {
|
|
|
120659
120755
|
});
|
|
120660
120756
|
els.seatProtocol.addEventListener("change", applySeatProtocolRules);
|
|
120661
120757
|
els.seatCaptureReasoning?.addEventListener("change", applySeatProtocolRules);
|
|
120758
|
+
els.seatReasoningEffort?.addEventListener("change", () => {
|
|
120759
|
+
seatReasoningEffortDraft = els.seatReasoningEffort.value;
|
|
120760
|
+
});
|
|
120662
120761
|
els.refreshModelsBtn?.addEventListener("click", () => void refreshSeatModelCatalog({ force: true }));
|
|
120663
120762
|
els.removeSeatBtn.addEventListener("click", () => {
|
|
120664
120763
|
if (editingSeatIndex == null || director && ["RUNNING", "PAUSED"].includes(director.status)) return;
|
package/dist/styles.css
CHANGED
|
@@ -492,7 +492,7 @@ label input, label select { margin-top: 5px; font-weight: 450; }
|
|
|
492
492
|
.split-footer { justify-content: space-between; }
|
|
493
493
|
.seat-footer-actions, .replay-share-actions { display: flex; gap: 7px; }
|
|
494
494
|
.seat-editor-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
|
495
|
-
.seat-model-field { grid-column: 1 / -1; }
|
|
495
|
+
.seat-model-field, .seat-effort-field { grid-column: 1 / -1; }
|
|
496
496
|
.model-input-wrap { margin-top: 6px; display: grid; grid-template-columns: minmax(0, 1fr) 38px; gap: 8px; }
|
|
497
497
|
.model-input-wrap input { margin: 0; }
|
|
498
498
|
.field-icon-button, .dialog-body .remove-connection { width: 38px; height: 38px; min-height: 38px; border-radius: 8px; }
|
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.17.0 — per-seat reasoning effort
|
|
4
|
+
|
|
5
|
+
- The seat editor now exposes **Reasoning effort** for OpenRouter models whose
|
|
6
|
+
catalogue entry advertises `supported_efforts` (for example
|
|
7
|
+
`deepseek/deepseek-v4.1-flash`: `max`, `high`, `low`, default `high`). The
|
|
8
|
+
options are populated from `GET /api/v1/models`, so an effort the model does
|
|
9
|
+
not accept can never be selected, and the catalogue default is labelled in the
|
|
10
|
+
list.
|
|
11
|
+
- Choosing an effort sends OpenRouter `reasoning: { effort }` for that seat.
|
|
12
|
+
Leaving it on **Arena default (capped)** keeps the existing
|
|
13
|
+
`reasoning: { max_tokens: 256, exclude: false }` request, so unmodified seats
|
|
14
|
+
and existing benchmark runs are unchanged.
|
|
15
|
+
- This addresses slow reasoners that exhaust the action clock and get
|
|
16
|
+
auto-folded: a seat can drop DeepSeek-class models from their `high` default to
|
|
17
|
+
`low` and answer inside the 30 s action + 30 s time-bank budget. The chosen
|
|
18
|
+
effort is recorded on `DECISION_START` and `DECISION` events.
|
|
19
|
+
- The truncation retry now lowers to an effort the catalogue actually supports
|
|
20
|
+
(falling back to the larger token cap when `low` is not offered) instead of
|
|
21
|
+
always sending `low`.
|
|
22
|
+
- Models whose catalogue entry marks reasoning as `mandatory` now get the larger
|
|
23
|
+
completion budget even when their name misses the reasoning heuristic (for
|
|
24
|
+
example `meta/muse-spark-1.3-contributor`), instead of being starved by the
|
|
25
|
+
320-token default.
|
|
26
|
+
- Those mandatory-reasoning models also show a warning in the seat editor when no
|
|
27
|
+
effort is selected, because the capped default does not constrain a model that
|
|
28
|
+
always reasons and it can still run past the action clock.
|
|
29
|
+
- The tool-to-JSON-schema fallback now reads OpenRouter's nested provider detail
|
|
30
|
+
(`error.metadata.raw`). Providers that reject every named `tool_choice` — Meta's
|
|
31
|
+
Muse Spark endpoints only accept `"auto"` — return a generic "Provider returned
|
|
32
|
+
error", which previously hid the real reason and forced an auto-fold on every
|
|
33
|
+
hand.
|
|
34
|
+
|
|
3
35
|
## 0.16.2 — blind multiplier defaults to ×2
|
|
4
36
|
|
|
5
37
|
- The default `blindMultiplier` is now `2` (was `1.5`), so blinds double at each
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pokertools-arena",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Browser-first AI poker benchmark powered by @pokertools/engine with generic OpenAI-compatible endpoints, optional OpenRouter/Jev Decisions routing, deterministic hierarchical decisions, paired fixed-state methodology, and reproducible reporting.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/app.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
legalAggressiveSizes, buildHierarchicalDecision, familyCriteria, sizeCriteria,
|
|
22
22
|
applyBenchmarkMode, renderDecisionState, aggregateActionProbabilitiesByFamily, probabilityStats, SIZE_LABELS,
|
|
23
23
|
isAggressiveType, familyForActionType, FAMILY_LABELS, decisionClockPhase, isReasoningModel, wantsReasoning,
|
|
24
|
+
reasoningEffortsFor, defaultReasoningEffort, requiresReasoning,
|
|
24
25
|
} from './lib/decision-core.js';
|
|
25
26
|
// Table sound effects. esbuild inlines these as data URLs at build time (see
|
|
26
27
|
// build.mjs), so the bundled and single-file builds both carry the audio and no
|
|
@@ -44,7 +45,7 @@ const els = {
|
|
|
44
45
|
setupDialog: $('#setupDialog'), setupForm: $('#setupForm'), closeSetup: $('#closeSetup'), setupError: $('#setupError'), saveSettingsBtn: $('#saveSettingsBtn'), seatSummary: $('#seatSummary'),
|
|
45
46
|
connectionsEditor: $('#connectionsEditor'), connectionRowTemplate: $('#connectionRowTemplate'), addConnectionBtn: $('#addConnectionBtn'),
|
|
46
47
|
seatDialog: $('#seatDialog'), seatForm: $('#seatForm'), closeSeat: $('#closeSeat'), seatDialogTitle: $('#seatDialogTitle'), seatLockNotice: $('#seatLockNotice'),
|
|
47
|
-
seatName: $('#seatName'), seatConnection: $('#seatConnection'), seatModel: $('#seatModel'), seatModelOptions: $('#seatModelOptions'), seatModelStatus: $('#seatModelStatus'), refreshModelsBtn: $('#refreshModelsBtn'), seatProtocol: $('#seatProtocol'), seatProvider: $('#seatProvider'), seatCaptureReasoning: $('#seatCaptureReasoning'), seatError: $('#seatError'), removeSeatBtn: $('#removeSeatBtn'), cancelSeatBtn: $('#cancelSeatBtn'), saveSeatBtn: $('#saveSeatBtn'),
|
|
48
|
+
seatName: $('#seatName'), seatConnection: $('#seatConnection'), seatModel: $('#seatModel'), seatModelOptions: $('#seatModelOptions'), seatModelStatus: $('#seatModelStatus'), refreshModelsBtn: $('#refreshModelsBtn'), seatProtocol: $('#seatProtocol'), seatProvider: $('#seatProvider'), seatCaptureReasoning: $('#seatCaptureReasoning'), seatReasoningEffort: $('#seatReasoningEffort'), seatReasoningEffortHelp: $('#seatReasoningEffortHelp'), seatError: $('#seatError'), removeSeatBtn: $('#removeSeatBtn'), cancelSeatBtn: $('#cancelSeatBtn'), saveSeatBtn: $('#saveSeatBtn'),
|
|
48
49
|
testsDialog: $('#testsDialog'), closeTests: $('#closeTests'), runTestsBtn: $('#runTestsBtn'), clearTestsBtn: $('#clearTestsBtn'), testsStatus: $('#testsStatus'), testsParticipants: $('#testsParticipants'), testsProgressLabel: $('#testsProgressLabel'), testsProgressFill: $('#testsProgressFill'), testsResults: $('#testsResults'),
|
|
49
50
|
replayDialog: $('#replayDialog'), closeReplay: $('#closeReplay'), replayTitle: $('#replayTitle'), replayBadge: $('#replayBadge'), replaySubtitle: $('#replaySubtitle'), replayOpponents: $('#replayOpponents'), replayStreet: $('#replayStreet'), replayBoard: $('#replayBoard'), replayPot: $('#replayPot'), replayHero: $('#replayHero'), replaySummary: $('#replaySummary'), replayAction: $('#replayAction'), replayReason: $('#replayReason'), replayReasoning: $('#replayReasoning'), replayHistory: $('#replayHistory'), replayLegal: $('#replayLegal'), replayLegalSummary: $('#replayLegalSummary'), replayShareStatus: $('#replayShareStatus'), copyReplayImage: $('#copyReplayImage'), shareReplayImage: $('#shareReplayImage'), saveReplayImage: $('#saveReplayImage'),
|
|
50
51
|
};
|
|
@@ -64,6 +65,9 @@ const WINNER_REVIEW_MS = 2_000;
|
|
|
64
65
|
let seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
|
|
65
66
|
let editingSeatIndex = null;
|
|
66
67
|
let seatNameAuto = false;
|
|
68
|
+
// The effort select is rebuilt whenever the model catalogue resolves, so the
|
|
69
|
+
// seat's intended choice is tracked separately from the live option list.
|
|
70
|
+
let seatReasoningEffortDraft = '';
|
|
67
71
|
let lobbyVisible = true;
|
|
68
72
|
let pendingAutostart = false;
|
|
69
73
|
let arenaMaxDecisions = 0;
|
|
@@ -356,6 +360,7 @@ function injectedEnvironmentConfig() {
|
|
|
356
360
|
protocol: String(player.protocol || 'tool'),
|
|
357
361
|
provider: String(player.provider || ''),
|
|
358
362
|
captureReasoning: Boolean(player.captureReasoning),
|
|
363
|
+
reasoningEffort: String(player.reasoningEffort || '').trim().toLowerCase(),
|
|
359
364
|
})),
|
|
360
365
|
};
|
|
361
366
|
}
|
|
@@ -628,7 +633,7 @@ function normalizeConfig(input = {}) {
|
|
|
628
633
|
if (!connIds.has(raw.connectionId)) throw new Error(`Connection missing for ${name}`);
|
|
629
634
|
const model = String(raw.model || '').trim();
|
|
630
635
|
if (!model) throw new Error(`Model missing for ${name}`);
|
|
631
|
-
return { id: `player-${lobbySeat + 1}`, seat: index, lobbySeat, name, connectionId: raw.connectionId, model, protocol: raw.protocol || 'tool', provider: String(raw.provider || '').trim(), captureReasoning: raw.captureReasoning === true, temperature: clamp(Number(raw.temperature) || 0.3, 0, 2) };
|
|
636
|
+
return { id: `player-${lobbySeat + 1}`, seat: index, lobbySeat, name, connectionId: raw.connectionId, model, protocol: raw.protocol || 'tool', provider: String(raw.provider || '').trim(), captureReasoning: raw.captureReasoning === true, reasoningEffort: String(raw.reasoningEffort || '').trim().toLowerCase(), temperature: clamp(Number(raw.temperature) || 0.3, 0, 2) };
|
|
632
637
|
}),
|
|
633
638
|
};
|
|
634
639
|
}
|
|
@@ -815,6 +820,7 @@ class TournamentDirector {
|
|
|
815
820
|
}
|
|
816
821
|
this.currentDecision = {
|
|
817
822
|
id: decisionId, playerId: agent.id, playerName: agent.name, seat, model: agent.model, connection: connection?.name ?? agent.connectionId, provider: agent.provider || 'auto',
|
|
823
|
+
reasoningEffort: agent.reasoningEffort || null,
|
|
818
824
|
protocol: effectiveProtocol(agent, connection), handNumber: this.handNumber, street: this.engine.state.street, position: positionForSeat(this.engine.state, seat),
|
|
819
825
|
startedAt, baseMs, timeBankMs: bankBefore, lowTimeMs: Math.round(this.config.lowTimeSeconds * 1000), lowTimeFraction: this.config.lowTimeFraction, pausedMs: 0, pausedAt: null, architecture,
|
|
820
826
|
hierarchy: hierarchy ? { families: hierarchy.families, stage1: hierarchy.stage1, stage2: hierarchy.stage2, aggressiveFamily: hierarchy.aggressiveFamily } : null,
|
|
@@ -915,6 +921,7 @@ class TournamentDirector {
|
|
|
915
921
|
playerId: agent.id, playerName: agent.name, connection: connection?.name,
|
|
916
922
|
protocol: result?.meta?.method ?? effectiveProtocol(agent, connection), requestedProtocol: effectiveProtocol(agent, connection), protocolFallback: result?.meta?.protocolFallback ?? null,
|
|
917
923
|
configuredModel: agent.model, resolvedModel: result?.model ?? agent.model, provider: agent.provider || 'auto',
|
|
924
|
+
reasoningEffort: agent.reasoningEffort || null,
|
|
918
925
|
action: { id: chosen.id, type: chosen.type, amount: chosen.amount, description: chosen.description }, forced,
|
|
919
926
|
legalActions: legalActions.map(a => ({ id: a.id, type: a.type, amount: a.amount ?? null, description: a.description })),
|
|
920
927
|
latencyMs: reportedLatency, primaryDecisionLatencyMs: result?.primaryDecisionLatencyMs ?? reportedLatency,
|
|
@@ -1129,6 +1136,7 @@ function renderModelOptions(models, currentValue = '') {
|
|
|
1129
1136
|
if (currentValue && !byId.has(currentValue)) byId.set(currentValue, { id: currentValue, name: currentValue });
|
|
1130
1137
|
const rows = [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
1131
1138
|
els.seatModelOptions.innerHTML = rows.map(model => `<option value="${escapeHtml(model.id)}" label="${escapeHtml(model.name || model.id)}"></option>`).join('');
|
|
1139
|
+
updateSeatReasoningEffortOptions();
|
|
1132
1140
|
}
|
|
1133
1141
|
async function refreshSeatModelCatalog({ force = false } = {}) {
|
|
1134
1142
|
const connection = fullConnectionById(els.seatConnection?.value);
|
|
@@ -1183,8 +1191,46 @@ function defaultSeatDraft(seatIndex) {
|
|
|
1183
1191
|
protocol: conn?.kind === 'typesafe' ? 'jev_native' : 'tool',
|
|
1184
1192
|
provider: '',
|
|
1185
1193
|
captureReasoning: false,
|
|
1194
|
+
reasoningEffort: '',
|
|
1186
1195
|
};
|
|
1187
1196
|
}
|
|
1197
|
+
// The effort choices come from the model catalogue, so an unsupported value can
|
|
1198
|
+
// never be selected. An empty value keeps the historical request shape.
|
|
1199
|
+
function updateSeatReasoningEffortOptions(selected) {
|
|
1200
|
+
const select = els.seatReasoningEffort;
|
|
1201
|
+
if (!select) return;
|
|
1202
|
+
const openRouter = isOpenRouterConnection(connectionById(els.seatConnection.value));
|
|
1203
|
+
const model = els.seatModel.value.trim();
|
|
1204
|
+
const efforts = openRouter ? reasoningEffortsFor(model) : [];
|
|
1205
|
+
const modelDefault = efforts.length ? defaultReasoningEffort(model) : null;
|
|
1206
|
+
const current = String(selected ?? seatReasoningEffortDraft ?? '').trim().toLowerCase();
|
|
1207
|
+
select.innerHTML = [
|
|
1208
|
+
'<option value="">Arena default (capped)</option>',
|
|
1209
|
+
...efforts.map(effort => `<option value="${escapeHtml(effort)}">${escapeHtml(effort)}${effort === modelDefault ? ' · model default' : ''}</option>`),
|
|
1210
|
+
].join('');
|
|
1211
|
+
// Until the catalogue resolves, an empty option list must not discard the
|
|
1212
|
+
// seat's saved choice; keep the draft so the picker restores it on rebuild.
|
|
1213
|
+
if (efforts.includes(current)) {
|
|
1214
|
+
select.value = current;
|
|
1215
|
+
seatReasoningEffortDraft = current;
|
|
1216
|
+
} else {
|
|
1217
|
+
select.value = '';
|
|
1218
|
+
if (!current) seatReasoningEffortDraft = '';
|
|
1219
|
+
}
|
|
1220
|
+
select.disabled = !efforts.length;
|
|
1221
|
+
const help = els.seatReasoningEffortHelp;
|
|
1222
|
+
if (!help) return;
|
|
1223
|
+
help.dataset.tone = '';
|
|
1224
|
+
if (!openRouter) help.textContent = 'Reasoning effort is available for OpenRouter models that advertise it.';
|
|
1225
|
+
else if (!efforts.length) help.textContent = 'This model does not advertise reasoning efforts; the capped default is used.';
|
|
1226
|
+
else if (!select.value && requiresReasoning(model)) {
|
|
1227
|
+
// The arena cap is a token limit, not an effort; models that always reason
|
|
1228
|
+
// ignore it and run at their own default, which is how they blow the clock.
|
|
1229
|
+
help.textContent = `This model always reasons and ignores the capped default, so it can exceed the action clock and auto-fold. Pick an effort — ${efforts.includes('minimal') ? 'minimal' : efforts[0]} is usually fastest.`;
|
|
1230
|
+
help.dataset.tone = 'bad';
|
|
1231
|
+
} else if (modelDefault) help.textContent = `Model default is ${modelDefault}. Lower it when a slow reasoner runs out of action clock and auto-folds.`;
|
|
1232
|
+
else help.textContent = 'Lower it when a slow reasoner runs out of action clock and auto-folds.';
|
|
1233
|
+
}
|
|
1188
1234
|
function applySeatProtocolRules() {
|
|
1189
1235
|
const conn = connectionById(els.seatConnection.value);
|
|
1190
1236
|
const kind = conn?.kind || 'openrouter';
|
|
@@ -1193,6 +1239,7 @@ function applySeatProtocolRules() {
|
|
|
1193
1239
|
const jevSeat = kind === 'typesafe' || jevViaOpenRouter;
|
|
1194
1240
|
document.querySelector('.seat-provider-field')?.classList.toggle('hidden', !openRouter || jevViaOpenRouter);
|
|
1195
1241
|
document.querySelector('.seat-reasoning-field')?.classList.toggle('hidden', jevSeat);
|
|
1242
|
+
document.querySelector('.seat-effort-field')?.classList.toggle('hidden', jevSeat);
|
|
1196
1243
|
if (els.seatCaptureReasoning) {
|
|
1197
1244
|
els.seatCaptureReasoning.disabled = jevSeat;
|
|
1198
1245
|
const alreadyReasoning = isReasoningModel(els.seatModel.value.trim());
|
|
@@ -1213,6 +1260,7 @@ function applySeatProtocolRules() {
|
|
|
1213
1260
|
els.seatProvider.disabled = !openRouter;
|
|
1214
1261
|
if (els.seatModel.value === 'jev-latest') els.seatModel.value = '';
|
|
1215
1262
|
}
|
|
1263
|
+
updateSeatReasoningEffortOptions();
|
|
1216
1264
|
}
|
|
1217
1265
|
function updateSeatSummary() {
|
|
1218
1266
|
const count = seatAssignments.filter(Boolean).length;
|
|
@@ -1239,13 +1287,16 @@ function openSeatEditor(seatIndex) {
|
|
|
1239
1287
|
els.seatProtocol.value = draft.protocol || 'tool';
|
|
1240
1288
|
els.seatProvider.value = draft.provider || '';
|
|
1241
1289
|
if (els.seatCaptureReasoning) els.seatCaptureReasoning.checked = Boolean(draft.captureReasoning);
|
|
1290
|
+
seatReasoningEffortDraft = String(draft.reasoningEffort || '').trim().toLowerCase();
|
|
1242
1291
|
applySeatProtocolRules();
|
|
1292
|
+
updateSeatReasoningEffortOptions(seatReasoningEffortDraft);
|
|
1243
1293
|
syncSeatNameFromModel();
|
|
1244
1294
|
void refreshSeatModelCatalog();
|
|
1245
1295
|
els.seatError.classList.add('hidden');
|
|
1246
1296
|
els.seatLockNotice.classList.toggle('hidden', !locked);
|
|
1247
1297
|
for (const control of [els.seatName, els.seatConnection, els.seatModel, els.seatProtocol, els.seatProvider]) control.disabled = locked || (control === els.seatProvider && (!isOpenRouterConnection(connectionById(els.seatConnection.value)) || isJevModel(els.seatModel.value)));
|
|
1248
1298
|
if (els.seatCaptureReasoning) els.seatCaptureReasoning.disabled = locked || els.seatCaptureReasoning.disabled;
|
|
1299
|
+
if (els.seatReasoningEffort) els.seatReasoningEffort.disabled = locked || els.seatReasoningEffort.disabled;
|
|
1249
1300
|
els.saveSeatBtn.disabled = locked;
|
|
1250
1301
|
els.removeSeatBtn.disabled = locked || !seatAssignments[seatIndex];
|
|
1251
1302
|
if (!els.seatDialog.open) els.seatDialog.showModal();
|
|
@@ -1261,6 +1312,7 @@ function readSeatDraft() {
|
|
|
1261
1312
|
protocol: els.seatProtocol.value,
|
|
1262
1313
|
provider: els.seatProvider.disabled ? '' : els.seatProvider.value.trim(),
|
|
1263
1314
|
captureReasoning: Boolean(els.seatCaptureReasoning && !els.seatCaptureReasoning.disabled && els.seatCaptureReasoning.checked),
|
|
1315
|
+
reasoningEffort: els.seatReasoningEffort && !els.seatReasoningEffort.disabled ? els.seatReasoningEffort.value.trim().toLowerCase() : '',
|
|
1264
1316
|
};
|
|
1265
1317
|
}
|
|
1266
1318
|
function readSeatPlayers() {
|
|
@@ -1290,6 +1342,7 @@ function restoreSeatAssignments(rows = []) {
|
|
|
1290
1342
|
protocol: player.protocol || 'tool',
|
|
1291
1343
|
provider: player.provider || '',
|
|
1292
1344
|
captureReasoning: player.captureReasoning === true,
|
|
1345
|
+
reasoningEffort: String(player.reasoningEffort || '').trim().toLowerCase(),
|
|
1293
1346
|
};
|
|
1294
1347
|
});
|
|
1295
1348
|
updateSeatSummary();
|
|
@@ -2019,7 +2072,7 @@ function renderStats(s) {
|
|
|
2019
2072
|
function tableRenderSignature(s) {
|
|
2020
2073
|
const running = ['RUNNING', 'PAUSED'].includes(s?.status);
|
|
2021
2074
|
if (lobbyVisible && !running) {
|
|
2022
|
-
return JSON.stringify(['lobby', seatAssignments.map(p => p ? [p.name, p.model, p.protocol, p.connectionId, p.provider, Boolean(p.captureReasoning)] : null)]);
|
|
2075
|
+
return JSON.stringify(['lobby', seatAssignments.map(p => p ? [p.name, p.model, p.protocol, p.connectionId, p.provider, Boolean(p.captureReasoning), p.reasoningEffort || ''] : null)]);
|
|
2023
2076
|
}
|
|
2024
2077
|
const t = s?.table;
|
|
2025
2078
|
if (!t) return JSON.stringify(['empty', s?.status || 'IDLE']);
|
|
@@ -3223,6 +3276,7 @@ els.seatModel.addEventListener('input', () => { applySeatProtocolRules(); syncSe
|
|
|
3223
3276
|
els.seatName.addEventListener('input', () => { seatNameAuto = false; });
|
|
3224
3277
|
els.seatProtocol.addEventListener('change', applySeatProtocolRules);
|
|
3225
3278
|
els.seatCaptureReasoning?.addEventListener('change', applySeatProtocolRules);
|
|
3279
|
+
els.seatReasoningEffort?.addEventListener('change', () => { seatReasoningEffortDraft = els.seatReasoningEffort.value; });
|
|
3226
3280
|
els.refreshModelsBtn?.addEventListener('click', () => void refreshSeatModelCatalog({ force: true }));
|
|
3227
3281
|
els.removeSeatBtn.addEventListener('click', () => {
|
|
3228
3282
|
if (editingSeatIndex == null || (director && ['RUNNING', 'PAUSED'].includes(director.status))) return;
|
package/src/index.html
CHANGED
|
@@ -304,6 +304,11 @@
|
|
|
304
304
|
<span class="checkbox-field"><input id="seatCaptureReasoning" type="checkbox" /> Capture raw model reasoning</span>
|
|
305
305
|
<span class="field-help seat-reasoning-help hidden">This model is not a reasoning model by default. Capturing its reasoning can be far slower (Gemma measured ~2s → 27–48s per decision) and is outside the standard benchmark track. Raise the action clock before using it.</span>
|
|
306
306
|
</label>
|
|
307
|
+
<label class="seat-effort-field">
|
|
308
|
+
<span class="field-label-row">Reasoning effort <button class="info-button inline-info" type="button" aria-label="About reasoning effort" data-tip="How much hidden thinking the model may spend. Higher effort is smarter but slower; a slow reasoner can exhaust the action clock and be forced to fold. Options come from the OpenRouter model catalogue, and an unset value keeps the arena's fixed reasoning cap.">i</button></span>
|
|
309
|
+
<select id="seatReasoningEffort"></select>
|
|
310
|
+
<span id="seatReasoningEffortHelp" class="field-help"></span>
|
|
311
|
+
</label>
|
|
307
312
|
</div>
|
|
308
313
|
</div>
|
|
309
314
|
<div id="seatError" class="setup-error hidden"></div>
|
package/src/lib/decision-core.js
CHANGED
|
@@ -176,8 +176,20 @@ export async function fetchStreamingJson(url, options, { maxRetries = 1, onDelta
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// OpenRouter reports upstream provider failures as a generic "Provider returned
|
|
180
|
+
// error" while the real reason sits in `error.metadata.raw`. Error handling that
|
|
181
|
+
// keys off message text (the tool_choice fallback) must read that nested detail
|
|
182
|
+
// or it silently stops firing for models like meta/muse-spark, whose provider
|
|
183
|
+
// rejects every named tool_choice.
|
|
184
|
+
export function requestErrorDetail(err) {
|
|
185
|
+
const parts = [summarizeError(err)];
|
|
186
|
+
const nested = err?.payload?.error?.metadata?.raw ?? err?.payload?.metadata?.raw;
|
|
187
|
+
if (typeof nested === 'string') parts.push(nested);
|
|
188
|
+
else if (nested) parts.push(String(JSON.stringify(nested)));
|
|
189
|
+
return parts.join(' ');
|
|
190
|
+
}
|
|
179
191
|
export function isUnsupportedToolChoiceError(err) {
|
|
180
|
-
const message =
|
|
192
|
+
const message = requestErrorDetail(err);
|
|
181
193
|
return [400, 404, 422].includes(Number(err?.status)) && /tool[_ -]?choice|tool call|tools?.*(?:unsupported|support)|no endpoints?.*support/i.test(message);
|
|
182
194
|
}
|
|
183
195
|
export function mapGet(mapish, key, fallback = 0) {
|
|
@@ -216,14 +228,33 @@ export const protocolCapabilityCache = new Map();
|
|
|
216
228
|
// catalogue says the model supports them. Unknown models keep the prior default
|
|
217
229
|
// so behaviour is unchanged when no catalogue has been loaded.
|
|
218
230
|
export const modelSupportedParameters = new Map();
|
|
231
|
+
|
|
232
|
+
// OpenRouter also advertises a per-model `reasoning` object
|
|
233
|
+
// ({ mandatory, supported_efforts, default_effort }) when the model exposes a
|
|
234
|
+
// reasoning-effort control. Sending an effort the catalogue does not list is a
|
|
235
|
+
// hard routing failure under `require_parameters`, and sending the wrong one is
|
|
236
|
+
// how a slow reasoner burns the action clock and gets auto-folded, so the
|
|
237
|
+
// catalogue is treated as the source of truth for what a seat may request.
|
|
238
|
+
export const modelReasoningCapabilities = new Map();
|
|
219
239
|
export const DEFAULT_TEMPERATURE = 0.3;
|
|
220
240
|
|
|
221
241
|
export function registerModelCapabilities(models = []) {
|
|
222
242
|
for (const entry of models ?? []) {
|
|
223
243
|
const id = typeof entry === 'string' ? entry : entry?.id;
|
|
244
|
+
if (!id) continue;
|
|
224
245
|
const parameters = typeof entry === 'object' && entry ? entry.supported_parameters : null;
|
|
225
|
-
if (
|
|
226
|
-
|
|
246
|
+
if (Array.isArray(parameters)) modelSupportedParameters.set(String(id), new Set(parameters.map(String)));
|
|
247
|
+
const reasoning = typeof entry === 'object' && entry ? entry.reasoning : null;
|
|
248
|
+
if (!reasoning || typeof reasoning !== 'object') continue;
|
|
249
|
+
const efforts = Array.isArray(reasoning.supported_efforts)
|
|
250
|
+
? reasoning.supported_efforts.map(value => String(value).trim().toLowerCase()).filter(Boolean)
|
|
251
|
+
: [];
|
|
252
|
+
const declaredDefault = String(reasoning.default_effort || '').trim().toLowerCase();
|
|
253
|
+
modelReasoningCapabilities.set(String(id), {
|
|
254
|
+
mandatory: reasoning.mandatory === true,
|
|
255
|
+
supportedEfforts: efforts,
|
|
256
|
+
defaultEffort: efforts.includes(declaredDefault) ? declaredDefault : null,
|
|
257
|
+
});
|
|
227
258
|
}
|
|
228
259
|
}
|
|
229
260
|
|
|
@@ -233,6 +264,43 @@ export function modelSupportsParameter(model, parameter) {
|
|
|
233
264
|
return supported.has(String(parameter));
|
|
234
265
|
}
|
|
235
266
|
|
|
267
|
+
export function reasoningCapabilities(model) {
|
|
268
|
+
return modelReasoningCapabilities.get(String(model || '')) ?? null;
|
|
269
|
+
}
|
|
270
|
+
export function reasoningEffortsFor(model) {
|
|
271
|
+
return reasoningCapabilities(model)?.supportedEfforts ?? [];
|
|
272
|
+
}
|
|
273
|
+
export function defaultReasoningEffort(model) {
|
|
274
|
+
return reasoningCapabilities(model)?.defaultEffort ?? null;
|
|
275
|
+
}
|
|
276
|
+
export function requiresReasoning(model) {
|
|
277
|
+
return reasoningCapabilities(model)?.mandatory === true;
|
|
278
|
+
}
|
|
279
|
+
// Returns the requested effort when the model can accept it. Unknown models
|
|
280
|
+
// (non-OpenRouter endpoints, or a catalogue that has not loaded yet) keep the
|
|
281
|
+
// requested value so an explicit seat choice is never silently dropped.
|
|
282
|
+
export function resolveReasoningEffort(model, requested) {
|
|
283
|
+
const value = String(requested || '').trim().toLowerCase();
|
|
284
|
+
if (!value) return null;
|
|
285
|
+
const capabilities = reasoningCapabilities(model);
|
|
286
|
+
if (!capabilities) return value;
|
|
287
|
+
return capabilities.supportedEfforts.includes(value) ? value : null;
|
|
288
|
+
}
|
|
289
|
+
// The reasoning payload for a normal request. An explicit, supported effort
|
|
290
|
+
// wins; otherwise the historical completed-reasoning cap is preserved so
|
|
291
|
+
// benchmark behaviour is unchanged for seats that never touch the control.
|
|
292
|
+
export function reasoningOverrideFor(agent) {
|
|
293
|
+
const effort = resolveReasoningEffort(agent?.model, agent?.reasoningEffort);
|
|
294
|
+
return effort ? { effort } : { max_tokens: 256, exclude: false };
|
|
295
|
+
}
|
|
296
|
+
// The truncation retry lowers effort so a long reasoner can still answer inside
|
|
297
|
+
// the action clock. Models that do not advertise `low` fall back to the cap.
|
|
298
|
+
export function truncationReasoningOverride(model) {
|
|
299
|
+
const capabilities = reasoningCapabilities(model);
|
|
300
|
+
if (!capabilities) return { effort: 'low' };
|
|
301
|
+
return capabilities.supportedEfforts.includes('low') ? { effort: 'low' } : { max_tokens: 4096, exclude: false };
|
|
302
|
+
}
|
|
303
|
+
|
|
236
304
|
// Returns the temperature to send, or undefined when the model's endpoints do
|
|
237
305
|
// not support the parameter. Only optional sampling parameters are gated this
|
|
238
306
|
// way; structural parameters (tools, response_format, max_tokens) stay enforced
|
|
@@ -275,7 +343,9 @@ export function isReasoningModel(model) {
|
|
|
275
343
|
// asked for hidden reasoning (Gemma measurably can), at a large latency cost, so
|
|
276
344
|
// it stays an explicit seat choice rather than a blanket default.
|
|
277
345
|
export function wantsReasoning(agent) {
|
|
278
|
-
return isReasoningModel(agent?.model) || agent?.captureReasoning === true
|
|
346
|
+
return isReasoningModel(agent?.model) || agent?.captureReasoning === true
|
|
347
|
+
|| reasoningCapabilities(agent?.model)?.mandatory === true
|
|
348
|
+
|| Boolean(resolveReasoningEffort(agent?.model, agent?.reasoningEffort));
|
|
279
349
|
}
|
|
280
350
|
export function effectiveProtocol(agent, connection) {
|
|
281
351
|
if (isOpenRouterConnection(connection) && isJevModel(agent?.model)) return 'jev_decisions';
|
|
@@ -1099,7 +1169,7 @@ export function buildOpenAICompatibleBody({ agent, connection, state, legalActio
|
|
|
1099
1169
|
}
|
|
1100
1170
|
if (isOpenRouter) {
|
|
1101
1171
|
body.provider = agent.provider ? { only: [agent.provider], allow_fallbacks: false, require_parameters: true } : { allow_fallbacks: true, require_parameters: true };
|
|
1102
|
-
if (wantsReasoning(agent)) body.reasoning = reasoning ??
|
|
1172
|
+
if (wantsReasoning(agent)) body.reasoning = reasoning ?? reasoningOverrideFor(agent);
|
|
1103
1173
|
}
|
|
1104
1174
|
if (stream) { body.stream = true; body.stream_options = { include_usage: true }; }
|
|
1105
1175
|
return body;
|
|
@@ -1160,7 +1230,7 @@ export async function decideOpenAICompatible({ agent, connection, state, legalAc
|
|
|
1160
1230
|
// return nothing. Retry once with a larger budget and a lower reasoning
|
|
1161
1231
|
// effort rather than forcing an automatic fallback.
|
|
1162
1232
|
truncationRetry = true;
|
|
1163
|
-
result = await execute(actualProtocol, { maxTokens: 4096, reasoning:
|
|
1233
|
+
result = await execute(actualProtocol, { maxTokens: 4096, reasoning: truncationReasoningOverride(agent.model) });
|
|
1164
1234
|
} else throw err;
|
|
1165
1235
|
}
|
|
1166
1236
|
const normalized = normalizeDecisionObject(result.obj, legalActions, decisionId);
|
|
@@ -1347,7 +1417,7 @@ async function requestChatStage({ agent, connection, schema, toolName, systemPro
|
|
|
1347
1417
|
}
|
|
1348
1418
|
if (isOpenRouter) {
|
|
1349
1419
|
body.provider = agent.provider ? { only: [agent.provider], allow_fallbacks: false, require_parameters: true } : { allow_fallbacks: true, require_parameters: true };
|
|
1350
|
-
if (wantsReasoning(agent)) body.reasoning = override.reasoning ??
|
|
1420
|
+
if (wantsReasoning(agent)) body.reasoning = override.reasoning ?? reasoningOverrideFor(agent);
|
|
1351
1421
|
}
|
|
1352
1422
|
if (onDelta) { body.stream = true; body.stream_options = { include_usage: true }; }
|
|
1353
1423
|
return body;
|
|
@@ -1388,7 +1458,7 @@ async function requestChatStage({ agent, connection, schema, toolName, systemPro
|
|
|
1388
1458
|
if (err?.truncated) {
|
|
1389
1459
|
// Retry once with a larger budget and lower reasoning effort instead of
|
|
1390
1460
|
// letting a truncated reasoning stage force an automatic fallback.
|
|
1391
|
-
const result = await execute(actual, { maxTokens: 4096, reasoning:
|
|
1461
|
+
const result = await execute(actual, { maxTokens: 4096, reasoning: truncationReasoningOverride(agent.model) });
|
|
1392
1462
|
return { ...result, protocolFallback: 'truncated→retry' };
|
|
1393
1463
|
}
|
|
1394
1464
|
throw err;
|
package/src/styles.css
CHANGED
|
@@ -492,7 +492,7 @@ label input, label select { margin-top: 5px; font-weight: 450; }
|
|
|
492
492
|
.split-footer { justify-content: space-between; }
|
|
493
493
|
.seat-footer-actions, .replay-share-actions { display: flex; gap: 7px; }
|
|
494
494
|
.seat-editor-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
|
495
|
-
.seat-model-field { grid-column: 1 / -1; }
|
|
495
|
+
.seat-model-field, .seat-effort-field { grid-column: 1 / -1; }
|
|
496
496
|
.model-input-wrap { margin-top: 6px; display: grid; grid-template-columns: minmax(0, 1fr) 38px; gap: 8px; }
|
|
497
497
|
.model-input-wrap input { margin: 0; }
|
|
498
498
|
.field-icon-button, .dialog-body .remove-connection { width: 38px; height: 38px; min-height: 38px; border-radius: 8px; }
|