@kevin5251984/guild 0.2.18 → 0.2.20

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.
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
9
9
  <link rel="icon" href="/favicon-16.png" type="image/png" sizes="16x16" />
10
10
  <title>Guild — 模型</title>
11
- <link rel="stylesheet" href="/style.css" />
11
+ <link rel="stylesheet" href="/style.css?v=aux-align" />
12
12
  <script src="/i18n.js"></script>
13
13
  </head>
14
14
  <body class="app settings-page" data-i18n-page="settings" data-section="main">
@@ -46,6 +46,7 @@
46
46
  </header>
47
47
  <div class="page-body">
48
48
  <p id="flash" class="muted"></p>
49
+ <p class="sub" data-i18n="settings.lede">第一次:先連訂閱或填 API key,再套用主模型。沒有模型就不能用。訂閱走 OAuth;API key 寫入 ~/.guild/models.json。</p>
49
50
 
50
51
  <section class="panel" data-sec-panel="main">
51
52
  <h2 data-i18n="settings.main">主模型</h2>
@@ -63,20 +64,17 @@
63
64
  </div>
64
65
  <p class="main-now" id="main-now"></p>
65
66
  <div class="toggles">
66
- <span>Reasoning</span>
67
- <select id="reasoning">
68
- <option value="minimal">Minimal</option>
69
- <option value="low">Low</option>
70
- <option value="medium">Medium</option>
71
- <option value="high">High</option>
72
- </select>
67
+ <span id="reasoning-wrap">
68
+ <span data-i18n="reasoning">推理強度</span>
69
+ <select id="reasoning"></select>
70
+ </span>
73
71
  <label><input type="checkbox" id="fast" /> Fast</label>
74
72
  </div>
75
73
  <div class="panel-head" style="margin-top:1rem">
76
74
  <h2 data-i18n="settings.aux">輔助模型</h2>
77
75
  <button type="button" class="linkish" id="reset-aux" data-i18n="settings.auxReset">全部改回主模型</button>
78
76
  </div>
79
- <p class="sub" data-i18n="settings.auxHint">Vision、Web extract、Compression 等每一列都寫出實際模型。跟主模型時仍顯示名稱,不是空白。</p>
77
+ <p class="sub" data-i18n="settings.auxHint">只有 Vision、Web extract、SubAgent 可以另選模型。其餘用途一律跟主模型。</p>
80
78
  <div id="aux"></div>
81
79
  </section>
82
80
 
@@ -144,6 +142,18 @@
144
142
  let auxRole = null;
145
143
  let keyTab = 0;
146
144
  const PRESETS = {
145
+ "opencode-free": {
146
+ id: "opencode-free",
147
+ name: "OpenCode Free",
148
+ baseUrl: "https://opencode.ai/zen/v1",
149
+ api: "openai-completions",
150
+ apiKey: "",
151
+ models: [
152
+ { id: "muse-spark-1.2-contributor-free", name: "Muse Spark 1.2 Contributor Free" },
153
+ { id: "laguna-s-2.1-free", name: "Laguna S 2.1 Free" },
154
+ { id: "mimo-v2.5-free", name: "Mimo V2.5 Free" },
155
+ ],
156
+ },
147
157
  openai: {
148
158
  id: "openai",
149
159
  name: "OpenAI",
@@ -260,11 +270,56 @@
260
270
  '<option value="' +
261
271
  escapeHtml(m.id) +
262
272
  '">' +
263
- escapeHtml(m.name || m.id) +
273
+ escapeHtml(prettyModelName(m.id, m.name)) +
264
274
  "</option>",
265
275
  )
266
276
  .join("");
267
277
  if (preferred && preferred.provider === psel.value) msel.value = preferred.model;
278
+ if (psel.id === "provider") fillReasoning();
279
+ }
280
+ function reasonLabel(value) {
281
+ if (value === "none") return t("reason.none");
282
+ if (value === "minimal") return t("reason.min");
283
+ if (value === "low") return t("reason.low");
284
+ if (value === "medium") return t("reason.mid");
285
+ if (value === "high") return t("reason.high");
286
+ if (value === "xhigh") return t("reason.xhigh");
287
+ if (value === "max") return t("reason.max");
288
+ return value || "";
289
+ }
290
+ function fillReasoning() {
291
+ const sel = document.getElementById("reasoning");
292
+ const wrap = document.getElementById("reasoning-wrap");
293
+ const ref = currentRef();
294
+ const group = pickerGroups().find((p) => p.id === (ref && ref.provider));
295
+ const model = ((group && group.models) || []).find(
296
+ (m) => m.id === (ref && ref.model),
297
+ );
298
+ const spec = (model && model.reasoning) || null;
299
+ const raw = spec && spec.supportedEfforts;
300
+ const choices = raw && raw.length
301
+ ? spec.mandatory
302
+ ? raw.filter((key) => key !== "none")
303
+ : raw.slice()
304
+ : [];
305
+ sel.innerHTML = choices
306
+ .map(
307
+ (value) =>
308
+ '<option value="' +
309
+ escapeHtml(value) +
310
+ '">' +
311
+ escapeHtml(reasonLabel(value)) +
312
+ "</option>",
313
+ )
314
+ .join("");
315
+ const want = data.reasoning;
316
+ if (want && choices.indexOf(want) >= 0) sel.value = want;
317
+ else if (spec && spec.defaultEffort && choices.indexOf(spec.defaultEffort) >= 0) {
318
+ sel.value = spec.defaultEffort;
319
+ } else if (choices.length) {
320
+ sel.value = choices[0];
321
+ }
322
+ if (wrap) wrap.hidden = !choices.length;
268
323
  }
269
324
  function currentRef() {
270
325
  const provider = document.getElementById("provider").value;
@@ -275,11 +330,27 @@
275
330
  function groupReady(id) {
276
331
  return Boolean((data.picker || []).find((p) => p.id === id && p.ready));
277
332
  }
333
+ function prettyModelName(id, given) {
334
+ const raw = String(id || "");
335
+ const label = String(given || "").trim();
336
+ if (label && label !== raw) return label;
337
+ const core = (raw.split("/").pop() || raw).trim();
338
+ const spaced = core
339
+ .replace(/[:_]+/g, "-")
340
+ .replace(/([A-Za-z])(\d)/g, "$1 $2")
341
+ .replace(/-/g, " ")
342
+ .replace(/\s+/g, " ")
343
+ .trim();
344
+ if (!spaced) return raw;
345
+ return spaced.replace(/\b([a-z])/g, function (ch) {
346
+ return ch.toUpperCase();
347
+ });
348
+ }
278
349
  function modelLabel(ref) {
279
350
  if (!ref || !ref.provider || !ref.model) return t("settings.noDefault");
280
351
  const group = pickerGroups().find((p) => p.id === ref.provider);
281
352
  const hit = ((group && group.models) || []).find((m) => m.id === ref.model);
282
- const name = (hit && hit.name) || ref.model;
353
+ const name = prettyModelName(ref.model, hit && hit.name);
283
354
  const vendor = (group && group.name) || ref.provider;
284
355
  return name + " · " + vendor;
285
356
  }
@@ -330,7 +401,7 @@
330
401
  }
331
402
  function renderPicker() {
332
403
  fillSelect(document.getElementById("provider"), pickerGroups(), data.default);
333
- document.getElementById("reasoning").value = data.reasoning || "medium";
404
+ fillReasoning();
334
405
  document.getElementById("fast").checked = Boolean(data.fast);
335
406
  const mainNow = document.getElementById("main-now");
336
407
  if (mainNow) {
@@ -388,12 +459,46 @@
388
459
  if (key.length <= 10) return key;
389
460
  return key.slice(0, 5) + "…" + key.slice(-5);
390
461
  }
462
+ function isKeylessProvider(id) {
463
+ return id === "opencode-free" || id === "free" || id === "opencode_free";
464
+ }
391
465
  function storedBadge(p) {
466
+ if (isKeylessProvider(p.id)) {
467
+ return '<span class="badge">' + t("settings.keyless") + "</span>";
468
+ }
392
469
  if (p.stored === "env") return '<span class="badge">env</span>';
393
470
  if (p.stored === "literal") return '<span class="badge">' + t("settings.filled") + "</span>";
394
471
  return '<span class="badge empty">' + t("settings.emptyKey") + "</span>";
395
472
  }
473
+ function probeFor(id) {
474
+ return ((data.probe || []).find((row) => row.id === id) || null);
475
+ }
476
+ function probeBadge(id) {
477
+ const row = probeFor(id);
478
+ if (!row) return "";
479
+ if (row.ok && row.status === 429) {
480
+ return '<span class="badge wait probe-flag">' + t("settings.probeBusy") + "</span>";
481
+ }
482
+ if (row.ok) return '<span class="badge probe-flag">' + t("settings.probeOk") + "</span>";
483
+ return '<span class="badge empty probe-flag">' + t("settings.probeFail") + "</span>";
484
+ }
485
+ function probeHint() {
486
+ const probe = data.probe || [];
487
+ if (!probe.length) return "";
488
+ const skip = probe.filter((row) => !row.ok);
489
+ if (!skip.length) return "";
490
+ return (
491
+ '<p class="hint">' +
492
+ t("settings.syncedSkip", {
493
+ ok: probe.filter((row) => row.ok).length,
494
+ names: skip.map((row) => row.id).join("、"),
495
+ }) +
496
+ "</p>"
497
+ );
498
+ }
396
499
  function cardHtml(p, index) {
500
+ const lock = isKeylessProvider(p.id);
501
+ const ro = lock ? " readonly" : "";
397
502
  const models = (p.models || [])
398
503
  .map((m, i) => {
399
504
  return (
@@ -401,7 +506,9 @@
401
506
  '" data-f="id" value="' + escapeHtml(m.id || "") +
402
507
  '" placeholder="model id" /><input data-p="' + index + '" data-m="' + i +
403
508
  '" data-f="name" value="' + escapeHtml(m.name || "") +
404
- '" placeholder="' + t("settings.displayName") + '" /><button type="button" class="secondary" data-del-model="' +
509
+ '" placeholder="' + t("settings.displayName") + '" />' +
510
+ probeBadge(m.id) +
511
+ '<button type="button" class="secondary" data-del-model="' +
405
512
  index + ":" + i + '">×</button></div>'
406
513
  );
407
514
  })
@@ -409,19 +516,38 @@
409
516
  return (
410
517
  '<div class="provider-card" data-index="' + index + '">' +
411
518
  '<div class="card-head"><h3>' + escapeHtml(p.name || p.id) + storedBadge(p) +
412
- '</h3><button type="button" class="secondary" data-del="' + index + '">' + t("settings.delete") + "</button></div>" +
413
- "<label>ID</label><input data-p=\"" + index + '" data-f="id" value="' + escapeHtml(p.id) + '" />' +
414
- "<label>" + t("settings.name") + "</label><input data-p=\"" + index + '" data-f="name" value="' + escapeHtml(p.name || "") + '" />' +
415
- "<label>baseUrl</label><input data-p=\"" + index + '" data-f="baseUrl" value="' + escapeHtml(p.baseUrl || "") + '" />' +
416
- '<div class="row-2"><div><label>API</label><select data-p="' + index + '" data-f="api">' +
519
+ "</h3>" +
520
+ (lock
521
+ ? ""
522
+ : '<button type="button" class="secondary" data-del-provider="' + index + '">' + t("settings.delete") + "</button>") +
523
+ "</div>" +
524
+ "<label>ID</label><input data-p=\"" + index + '" data-f="id" value="' + escapeHtml(p.id) + '"' + ro + " />" +
525
+ "<label>" + t("settings.name") + "</label><input data-p=\"" + index + '" data-f="name" value="' + escapeHtml(p.name || "") + '"' + ro + " />" +
526
+ "<label>baseUrl</label><input data-p=\"" + index + '" data-f="baseUrl" value="' + escapeHtml(p.baseUrl || "") + '"' + ro + " />" +
527
+ '<div class="row-2"><div><label>API</label><select data-p="' + index + '" data-f="api"' +
528
+ (lock ? " disabled" : "") + ">" +
417
529
  '<option value="openai-completions"' + (p.api !== "anthropic-messages" && p.api !== "openai-responses" ? " selected" : "") + ">openai-completions</option>" +
418
530
  '<option value="anthropic-messages"' + (p.api === "anthropic-messages" ? " selected" : "") + ">anthropic-messages</option>" +
419
531
  '<option value="openai-responses"' + (p.api === "openai-responses" ? " selected" : "") + ">openai-responses</option></select></div>" +
420
- '<div><label>API key</label><input type="text" data-p="' + index + '" data-f="apiKey" spellcheck="false" autocomplete="off" autocapitalize="off" value="' +
421
- escapeHtml(p.apiKeyPreview || "") + '" placeholder="$ENV_VAR sk-…" /></div></div>' +
422
- "<label>" + t("model") + "</label>" + models +
532
+ (isKeylessProvider(p.id)
533
+ ? '<div><label>API key</label><p class="hint">' + t("settings.keyless") + "</p></div>"
534
+ : '<div><label>API key</label><input type="text" data-p="' + index + '" data-f="apiKey" spellcheck="false" autocomplete="off" autocapitalize="off" value="' +
535
+ escapeHtml(p.apiKeyPreview || "") + '" placeholder="$ENV_VAR 或 sk-…" /></div>') +
536
+ "</div>" +
537
+ (isKeylessProvider(p.id)
538
+ ? '<p class="hint">' + t("settings.opencodeFreeHint") + "</p>"
539
+ : "") +
540
+ '<div class="model-head"><label>' + t("model") + "</label>" +
541
+ (isKeylessProvider(p.id)
542
+ ? '<button type="button" class="sync-free" data-sync-free="1"><span class="sync-ico" aria-hidden="true">↻</span>' + t("settings.sync") + "</button>"
543
+ : "") +
544
+ "</div>" +
545
+ models +
546
+ (lock ? probeHint() : "") +
423
547
  '<button type="button" class="secondary" data-add-model="' + index + '" style="margin-top:0.55rem">' + t("settings.addModel") + "</button>" +
424
- '<div class="card-foot"><p class="hint">' + t("settings.saveKeyHint") + "</p>" +
548
+ '<div class="card-foot"><p class="hint">' +
549
+ t(isKeylessProvider(p.id) ? "settings.saveKeylessHint" : "settings.saveKeyHint") +
550
+ "</p>" +
425
551
  '<button type="button" data-save-keys="' + index + '">' +
426
552
  t("settings.saveKey") +
427
553
  "</button></div></div>"
@@ -434,11 +560,19 @@
434
560
  const p = (data.providers || [])[index];
435
561
  if (!p) return;
436
562
  const id = card.querySelector('[data-f="id"]').value.trim();
437
- if (id) p.id = id;
438
- p.name = card.querySelector('[data-f="name"]').value.trim();
439
- p.baseUrl = card.querySelector('[data-f="baseUrl"]').value.trim();
440
- p.api = card.querySelector('[data-f="api"]').value;
441
- const key = card.querySelector('[data-f="apiKey"]').value.trim();
563
+ if (isKeylessProvider(p.id) || isKeylessProvider(id)) {
564
+ p.id = "opencode-free";
565
+ p.name = "OpenCode Free";
566
+ p.baseUrl = "https://opencode.ai/zen/v1";
567
+ p.api = "openai-completions";
568
+ } else {
569
+ if (id) p.id = id;
570
+ p.name = card.querySelector('[data-f="name"]').value.trim();
571
+ p.baseUrl = card.querySelector('[data-f="baseUrl"]').value.trim();
572
+ p.api = card.querySelector('[data-f="api"]').value;
573
+ }
574
+ const keyEl = card.querySelector('[data-f="apiKey"]');
575
+ const key = keyEl ? keyEl.value.trim() : "";
442
576
  if (key && key !== (p.apiKeyPreview || "")) {
443
577
  p.apiKey = key;
444
578
  p.apiKeyPreview = maskApiKey(key);
@@ -454,8 +588,7 @@
454
588
  });
455
589
  p.models = models;
456
590
  }
457
- function collectKeys() {
458
- flushVisibleCard();
591
+ function snapshotProviders() {
459
592
  const providers = {};
460
593
  (data.providers || []).forEach((p) => {
461
594
  if (!p.id) return;
@@ -469,6 +602,10 @@
469
602
  });
470
603
  return providers;
471
604
  }
605
+ function collectKeys() {
606
+ flushVisibleCard();
607
+ return snapshotProviders();
608
+ }
472
609
  function presetLabel(key, preset) {
473
610
  return key === "custom" ? t("settings.custom") : (preset && preset.name) || key;
474
611
  }
@@ -601,6 +738,7 @@
601
738
  data.default,
602
739
  );
603
740
  });
741
+ document.getElementById("model").addEventListener("change", fillReasoning);
604
742
  document.getElementById("apply").addEventListener("click", () => applyPicker());
605
743
  document.getElementById("reset-aux").addEventListener("click", async () => {
606
744
  await applyPicker({ aux: {} });
@@ -730,6 +868,68 @@
730
868
  document.getElementById("add-provider-cancel").addEventListener("click", () => {
731
869
  document.getElementById("add-provider-dialog").close();
732
870
  });
871
+ async function syncOpenCodeFree() {
872
+ const btn = document.querySelector("[data-sync-free]");
873
+ if (btn) btn.disabled = true;
874
+ flash("", t("settings.syncing"));
875
+ try {
876
+ const res = await fetch("/settings/models/opencode-free/sync", {
877
+ method: "POST",
878
+ });
879
+ const body = await res.json().catch(() => ({}));
880
+ if (!res.ok) {
881
+ flash("error", body.error || t("settings.syncFailed"));
882
+ return;
883
+ }
884
+ data = body;
885
+ const probe = Array.isArray(body.probe) ? body.probe : [];
886
+ const ok = probe.filter((row) => row.ok).length;
887
+ const skip = probe.filter((row) => !row.ok);
888
+ render();
889
+ if (probe.length && !ok) {
890
+ flash("error", t("settings.syncedNone"));
891
+ } else if (skip.length) {
892
+ flash(
893
+ "ok",
894
+ t("settings.syncedSkip", {
895
+ ok: ok,
896
+ names: skip.map((row) => row.id).join("、"),
897
+ }),
898
+ );
899
+ } else {
900
+ const n =
901
+ ok ||
902
+ (((data.picker || []).find((p) => p.id === "opencode-free") || {}).models || [])
903
+ .length;
904
+ flash("ok", t("settings.synced", { n: n }));
905
+ }
906
+ } catch (err) {
907
+ flash("error", err.message || t("settings.syncFailed"));
908
+ } finally {
909
+ if (btn) btn.disabled = false;
910
+ }
911
+ }
912
+ async function deleteProvider(index) {
913
+ const doomed = (data.providers || [])[index];
914
+ if (!doomed || isKeylessProvider(doomed.id)) return;
915
+ const name = doomed.name || doomed.id;
916
+ data.providers.splice(index, 1);
917
+ if (keyTab >= data.providers.length) keyTab = Math.max(0, data.providers.length - 1);
918
+ const res = await fetch("/settings/models", {
919
+ method: "PUT",
920
+ headers: { "content-type": "application/json" },
921
+ body: JSON.stringify({ providers: snapshotProviders() }),
922
+ });
923
+ const body = await res.json().catch(() => ({}));
924
+ if (!res.ok) {
925
+ flash("error", body.error || t("deleteFailed"));
926
+ await load();
927
+ return;
928
+ }
929
+ data = body;
930
+ render();
931
+ flash("health", t("settings.deletedProvider", { name: name }));
932
+ }
733
933
  async function saveProviderCard(index) {
734
934
  const card = document.querySelector('.provider-card[data-index="' + index + '"]');
735
935
  const label = card
@@ -762,17 +962,21 @@
762
962
  if (p) input.value = p.apiKeyPreview || "";
763
963
  });
764
964
  document.getElementById("providers").addEventListener("click", (event) => {
765
- const el = event.target;
965
+ const el = event.target.closest ? event.target : event.target.parentElement;
966
+ if (!el || !el.closest) return;
766
967
  if (el.closest("[data-save-keys]")) {
767
968
  const i = Number(el.closest("[data-save-keys]").getAttribute("data-save-keys"));
768
969
  saveProviderCard(i);
769
970
  return;
770
971
  }
771
- if (el.closest("[data-del]")) {
772
- const i = Number(el.closest("[data-del]").getAttribute("data-del"));
773
- data.providers.splice(i, 1);
774
- if (keyTab >= data.providers.length) keyTab = Math.max(0, data.providers.length - 1);
775
- render();
972
+ if (el.closest("[data-sync-free]")) {
973
+ syncOpenCodeFree();
974
+ return;
975
+ }
976
+ if (el.closest("[data-del-provider]")) {
977
+ const i = Number(el.closest("[data-del-provider]").getAttribute("data-del-provider"));
978
+ deleteProvider(i);
979
+ return;
776
980
  }
777
981
  if (el.closest("[data-add-model]")) {
778
982
  flushVisibleCard();
@@ -95,7 +95,12 @@
95
95
  return;
96
96
  }
97
97
  const button = document.getElementById("ai-generate");
98
+ if (button.disabled || button.getAttribute("aria-busy") === "true") return;
98
99
  button.disabled = true;
100
+ button.setAttribute("aria-busy", "true");
101
+ button.classList.add("is-busy");
102
+ const idle = button.textContent;
103
+ button.textContent = t("studio.generating");
99
104
  try {
100
105
  const response = await fetch("/generate", {
101
106
  method: "POST",
@@ -115,6 +120,9 @@
115
120
  );
116
121
  } finally {
117
122
  button.disabled = false;
123
+ button.removeAttribute("aria-busy");
124
+ button.classList.remove("is-busy");
125
+ button.textContent = idle;
118
126
  }
119
127
  });
120
128