pi-custom-provider-model 0.1.1 → 0.1.2

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A small, local browser panel for configuring custom providers in [Pi](https://pi.dev). English and Indonesian UI. Built against the official **Pi 0.85.1** public extension and SDK APIs.
4
4
 
5
- **v0.1.0 local prototype.** npm publication and package scope are not configured yet. `private: true` is intentional during development.
5
+ Published on npm as [`pi-custom-provider-model`](https://www.npmjs.com/package/pi-custom-provider-model). Development uses a local path install; tagged releases are published through GitHub Actions.
6
6
 
7
7
  ## What works
8
8
 
@@ -17,11 +17,23 @@ A small, local browser panel for configuring custom providers in [Pi](https://pi
17
17
  - Preserve existing unknown settings/model overrides and JSON comments when editing; detect stale writes and retain the last 10 `models.json` backups.
18
18
  - Show credential source without returning saved keys or headers to the browser.
19
19
 
20
- ## Try the prototype
20
+ ## Install
21
21
 
22
- Requires Node **22.19+** and Pi **0.85.1+**. Compatibility is currently tested with 0.85.1 on Windows/Edge; other versions/platforms need verification before release.
22
+ Requires Node **22.19+** and Pi **0.85.1+**. Compatibility is currently tested with 0.85.1 on Windows/Edge.
23
23
 
24
- From this project directory:
24
+ For normal use:
25
+
26
+ ```powershell
27
+ pi install npm:pi-custom-provider-model
28
+ ```
29
+
30
+ To update later:
31
+
32
+ ```powershell
33
+ pi update npm:pi-custom-provider-model
34
+ ```
35
+
36
+ For a one-off local development run from this project directory:
25
37
 
26
38
  ```powershell
27
39
  npm install --ignore-scripts
@@ -168,7 +180,7 @@ npm run pack:check
168
180
 
169
181
  Tests use isolated temporary directories and mock endpoints. Real gateway compatibility and RAM footprint have not been benchmarked yet.
170
182
 
171
- See [`docs/design.md`](docs/design.md) for the version-pinned official documentation and publication plan. Future distribution will be `pi install npm:@your-scope/package-name`, with a `pi` manifest and the `pi-package` keyword for the Pi catalog.
183
+ See [`docs/design.md`](docs/design.md) for the version-pinned official documentation and release design. Tagged releases are published to npm as `pi-custom-provider-model` through GitHub Actions.
172
184
 
173
185
  ## License
174
186
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-custom-provider-model",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A local browser-based custom provider and API-key manager for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/service.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { ModelRuntime, readStoredCredential, SettingsManager, VERSION } from "@earendil-works/pi-coding-agent";
3
5
  import { ConfigStore, parseDocument, patch, readText } from "./storage.ts";
4
6
  import { discover, endpointUrls, requestHeaders } from "./discovery.ts";
@@ -6,6 +8,8 @@ import { CAPABILITIES, probeModel, type Capability } from "./probes.ts";
6
8
  import { LimitCatalog, LIMIT_FIELDS } from "./limits.ts";
7
9
  import { APIS, AppError, mergeCompat, validateDraft, validateId, type JsonObject, type ModelInput, type ProviderDraft } from "./types.ts";
8
10
 
11
+ const MANAGER_VERSION = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8")).version as string;
12
+
9
13
  function visibleCompat(entry: JsonObject): ModelInput["compat"] {
10
14
  return typeof entry.compat?.supportsDeveloperRole === "boolean" ? { supportsDeveloperRole: entry.compat.supportsDeveloperRole } : undefined;
11
15
  }
@@ -50,7 +54,7 @@ export class ProviderService {
50
54
  };
51
55
  });
52
56
  return {
53
- dir: this.dir, piVersion: VERSION, revision: current.revision, providers,
57
+ dir: this.dir, piVersion: VERSION, managerVersion: MANAGER_VERSION, revision: current.revision, providers,
54
58
  defaultProvider: settings.defaultProvider ?? "", defaultModel: settings.defaultModel ?? "",
55
59
  reservedIds: [...this.reserved],
56
60
  };
@@ -175,7 +179,9 @@ export class ProviderService {
175
179
  const current = await this.store.read();
176
180
  this.assertEditable(id, current.data.providers[id]);
177
181
  const settings = parseDocument(await readText(join(this.dir, "settings.json")));
178
- if (settings.defaultProvider === id) throw new AppError("Choose another default provider before deleting this one.");
182
+ if (settings.defaultProvider === id) {
183
+ throw new AppError("This provider is Pi's default and cannot be deleted yet. Open another provider, choose Set default on one of its models, then try again.");
184
+ }
179
185
  await this.store.update(expected, (text) => patch(text, ["providers", id], undefined));
180
186
  await this.onSaved?.();
181
187
  return { ok: true, message: "Provider removed. Its credentials remain in auth.json; remove them separately if needed.", state: await this.state() };
package/web/app.js CHANGED
@@ -1,10 +1,10 @@
1
1
  const $ = (id) => document.getElementById(id);
2
2
  const translations = {
3
3
  id: {
4
- local:"Workspace lokal",providers:"Provider",addProvider:"+ Tambah provider",configuration:"KONFIGURASI",docs:"Dokumentasi resmi Pi ↗",connections:"KONEKSI",subtitle:"Endpoint dan model pilihanmu. Konfigurasi asli Pi.",refresh:"Muat ulang konfigurasi",connection:"Koneksi",native:"Format Pi",readonly:"Provider ini dikelola Pi atau integrasi lain. Buat provider custom untuk mengedit di sini.",providerId:"ID provider",idHelp:"Nama unik di Pi, misalnya my-gateway.",protocol:"Protokol API",protocolHelp:"Pilih protokol yang didukung endpoint.",requestPreview:"PREVIEW REQUEST",credentialSource:"SUMBER KREDENSIAL",removeKey:"Hapus key tersimpan",bearer:"Tambahkan header Authorization: Bearer secara eksplisit (Pi authHeader)",advanced:"Field lanjutan dan header custom yang sudah ada dipertahankan. Override per model dapat mengubah request efektif.",testConnection:"Tes koneksi",fetchModels:"Tarik model ↗",testHint:"Memeriksa daftar model. Tanpa request generasi.",models:"Model",addModel:"+ Model manual",modelHint:"Pilih model untuk disimpan. ID yang terdaftar belum menjamin dukungan chat, vision, atau tools.",selectAll:"Pilih yang terlihat",modelId:"ID model",context:"Konteks",input:"Input",actions:"Aksi",noModels:"Hubungkan model pertamamu",noModelsHint:"Tarik model dari endpoint atau tambahkan ID secara manual.",saveHelp:"Model → models.json · Key → auth.json",deleteProvider:"Hapus provider",save:"Simpan provider",footnote:"Konfigurasi disimpan lokal. Buka /model di Pi untuk memilih model. Default berlaku saat Pi dibuka kembali.",editModel:"Pengaturan model",displayName:"Nama tampilan (opsional)",maxOutput:"Maksimal token output",vision:"Input gambar",reasoning:"Dukungan reasoning",defaultText:"Default Pi (teks)",defaultOff:"Default Pi (nonaktif)",textOnly:"Teks saja",textImage:"Teks + gambar",off:"Nonaktif",on:"Aktif",metadataHelp:"Biarkan nilai yang belum diketahui memakai default Pi. Mengaktifkan opsi tidak menambahkan kemampuan pada model upstream.",applyModel:"Terapkan pengaturan",newProvider:"Provider baru",selected:"dipilih",edit:"Edit",test:"Tes",setDefault:"Jadikan default",default:"Default",remove:"Hapus",unsaved:"Ada perubahan yang belum disimpan. Lanjutkan?",keyHelp:"Kosongkan untuk mempertahankan key. Key baru disimpan di auth.json.",openaiHelp:"URL asli Pi. Biasanya diakhiri /v1; SDK menambahkan /chat/completions atau /responses.",anthropicHelp:"URL asli Pi. Biasanya tanpa /v1 di akhir; SDK menambahkan /v1/messages. Prefix gateway seperti /anthropic tetap dipakai.",busy:"Sedang memproses…",saved:"Provider tersimpan. Buka /model di Pi untuk memilihnya.",confirmDelete:"Hapus konfigurasi provider ini? Kredensialnya tetap tersimpan.",confirmKey:"Hapus key provider dari auth.json? Sumber key lain tetap dapat dipakai Pi.",confirmTest:"Kirim prompt singkat ke model ini? Tes memakai token, tanpa mengirim file atau percakapan proyek.",defaultSaved:"Default tersimpan untuk sesi Pi berikutnya.",fetchDone:"Daftar model diterima",allFetched:"Semua halaman yang dilaporkan endpoint telah diambil.",removeModel:"Hapus model ini dari pilihan? Perubahan berlaku setelah disimpan.",missingToken:"Buka manager dari link yang ditampilkan Pi.",search:"Cari ID model…",noProviders:"Belum ada provider custom.",saveFirst:"Simpan provider dahulu.",duplicate:"ID model sudah ada.",fallback:"default Pi",keyRemoved:"Key di auth.json dihapus.",working:"Bekerja",connectionOk:"Endpoint daftar model merespons.",refreshDone:"Konfigurasi dimuat ulang.",defaultHelp:"Berlaku saat Pi dibuka kembali.",partial:"Sebagian hasil",matches:"terlihat",
4
+ local:"Workspace lokal",providers:"Provider",addProvider:"+ Tambah provider",configuration:"KONFIGURASI",docs:"Dokumentasi resmi Pi ↗",connections:"KONEKSI",subtitle:"Endpoint dan model pilihanmu. Konfigurasi asli Pi.",refresh:"Muat ulang konfigurasi",connection:"Koneksi",native:"Format Pi",readonly:"Provider ini dikelola Pi atau integrasi lain. Buat provider custom untuk mengedit di sini.",defaultProviderBadge:"Default Pi",defaultProviderNote:"Provider ini adalah default Pi dan belum dapat dihapus. Untuk menghapusnya, buka provider lain lalu pilih ‘Jadikan default’ pada salah satu modelnya.",defaultActionHelp:"‘Jadikan default’ pada Aksi model menetapkan model dan provider tersebut sebagai default Pi untuk pembukaan berikutnya.",providerId:"ID provider",idHelp:"Nama unik di Pi, misalnya my-gateway.",protocol:"Protokol API",protocolHelp:"Pilih protokol yang didukung endpoint.",requestPreview:"PREVIEW REQUEST",credentialSource:"SUMBER KREDENSIAL",removeKey:"Hapus key tersimpan",bearer:"Tambahkan header Authorization: Bearer secara eksplisit (Pi authHeader)",advanced:"Field lanjutan dan header custom yang sudah ada dipertahankan. Override per model dapat mengubah request efektif.",testConnection:"Tes koneksi",fetchModels:"Tarik model ↗",testHint:"Memeriksa daftar model. Tanpa request generasi.",models:"Model",addModel:"+ Model manual",modelHint:"Pilih model untuk disimpan. ID yang terdaftar belum menjamin dukungan chat, vision, atau tools.",selectAll:"Pilih yang terlihat",modelId:"ID model",context:"Konteks",input:"Input",actions:"Aksi",noModels:"Hubungkan model pertamamu",noModelsHint:"Tarik model dari endpoint atau tambahkan ID secara manual.",saveHelp:"Model → models.json · Key → auth.json",deleteProvider:"Hapus provider",save:"Simpan provider",footnote:"Konfigurasi disimpan lokal. Buka /model di Pi untuk memilih model. Default berlaku saat Pi dibuka kembali.",editModel:"Pengaturan model",displayName:"Nama tampilan (opsional)",maxOutput:"Maksimal token output",vision:"Input gambar",reasoning:"Dukungan reasoning",defaultText:"Default Pi (teks)",defaultOff:"Default Pi (nonaktif)",textOnly:"Teks saja",textImage:"Teks + gambar",off:"Nonaktif",on:"Aktif",metadataHelp:"Biarkan nilai yang belum diketahui memakai default Pi. Mengaktifkan opsi tidak menambahkan kemampuan pada model upstream.",applyModel:"Terapkan pengaturan",newProvider:"Provider baru",selected:"dipilih",edit:"Edit",test:"Tes",setDefault:"Jadikan default",default:"Default",remove:"Hapus",unsaved:"Ada perubahan yang belum disimpan. Lanjutkan?",keyHelp:"Kosongkan untuk mempertahankan key. Key baru disimpan di auth.json.",openaiHelp:"URL asli Pi. Biasanya diakhiri /v1; SDK menambahkan /chat/completions atau /responses.",anthropicHelp:"URL asli Pi. Biasanya tanpa /v1 di akhir; SDK menambahkan /v1/messages. Prefix gateway seperti /anthropic tetap dipakai.",busy:"Sedang memproses…",saved:"Provider tersimpan. Buka /model di Pi untuk memilihnya.",confirmDelete:"Hapus konfigurasi provider ini? Kredensialnya tetap tersimpan.",confirmKey:"Hapus key provider dari auth.json? Sumber key lain tetap dapat dipakai Pi.",confirmTest:"Kirim prompt singkat ke model ini? Tes memakai token, tanpa mengirim file atau percakapan proyek.",defaultSaved:"Default tersimpan untuk sesi Pi berikutnya.",fetchDone:"Daftar model diterima",allFetched:"Semua halaman yang dilaporkan endpoint telah diambil.",removeModel:"Hapus model ini dari pilihan? Perubahan berlaku setelah disimpan.",missingToken:"Buka manager dari link yang ditampilkan Pi.",search:"Cari ID model…",noProviders:"Belum ada provider custom.",saveFirst:"Simpan provider dahulu.",duplicate:"ID model sudah ada.",fallback:"default Pi",keyRemoved:"Key di auth.json dihapus.",working:"Bekerja",connectionOk:"Endpoint daftar model merespons.",refreshDone:"Konfigurasi dimuat ulang.",defaultHelp:"Berlaku saat Pi dibuka kembali.",partial:"Sebagian hasil",matches:"terlihat",
5
5
  },
6
6
  en: {
7
- newProvider:"New provider",selected:"selected",edit:"Edit",test:"Test",setDefault:"Set default",default:"Default",remove:"Remove",unsaved:"You have unsaved changes. Continue?",keyHelp:"Leave blank to keep the current key. New keys are stored in auth.json.",openaiHelp:"Pi-native URL. Usually ends in /v1; the SDK appends /chat/completions or /responses.",anthropicHelp:"Pi-native URL. Usually no trailing /v1; the SDK appends /v1/messages. Gateway prefixes such as /anthropic are preserved.",busy:"Working…",saved:"Provider saved. Open /model in Pi to select it.",confirmDelete:"Delete this provider configuration? Its credentials will be kept.",confirmKey:"Remove this provider's key from auth.json? Pi may still use other configured key sources.",confirmTest:"Send a short prompt to this model? This uses tokens, but sends no project files or chat history.",defaultSaved:"Default saved for new Pi launches.",fetchDone:"Model list received",allFetched:"All pages reported by the endpoint have been fetched.",removeModel:"Remove this model from the selection? Changes take effect after saving.",missingToken:"Open the manager using the link printed by Pi.",search:"Search model IDs…",noProviders:"No custom providers yet.",saveFirst:"Save the provider first.",duplicate:"This model ID already exists.",fallback:"Pi default",keyRemoved:"Saved key removed from auth.json.",working:"Working",connectionOk:"Model-list endpoint responded.",refreshDone:"Configuration reloaded.",defaultHelp:"Applies to new Pi launches.",partial:"Partial results",matches:"visible",
7
+ newProvider:"New provider",selected:"selected",edit:"Edit",test:"Test",setDefault:"Set default",default:"Default",defaultProviderBadge:"Pi default",defaultProviderNote:"This provider is Pi's default and cannot be deleted yet. To delete it, open another provider and choose ‘Set default’ on one of its models.",defaultActionHelp:"‘Set default’ in a model's Actions makes that model and provider Pi's default for new launches.",remove:"Remove",unsaved:"You have unsaved changes. Continue?",keyHelp:"Leave blank to keep the current key. New keys are stored in auth.json.",openaiHelp:"Pi-native URL. Usually ends in /v1; the SDK appends /chat/completions or /responses.",anthropicHelp:"Pi-native URL. Usually no trailing /v1; the SDK appends /v1/messages. Gateway prefixes such as /anthropic are preserved.",busy:"Working…",saved:"Provider saved. Open /model in Pi to select it.",confirmDelete:"Delete this provider configuration? Its credentials will be kept.",confirmKey:"Remove this provider's key from auth.json? Pi may still use other configured key sources.",confirmTest:"Send a short prompt to this model? This uses tokens, but sends no project files or chat history.",defaultSaved:"Default saved for new Pi launches.",fetchDone:"Model list received",allFetched:"All pages reported by the endpoint have been fetched.",removeModel:"Remove this model from the selection? Changes take effect after saving.",missingToken:"Open the manager using the link printed by Pi.",search:"Search model IDs…",noProviders:"No custom providers yet.",saveFirst:"Save the provider first.",duplicate:"This model ID already exists.",fallback:"Pi default",keyRemoved:"Saved key removed from auth.json.",working:"Working",connectionOk:"Model-list endpoint responded.",refreshDone:"Configuration reloaded.",defaultHelp:"Applies to new Pi launches.",partial:"Partial results",matches:"visible",
8
8
  },
9
9
  };
10
10
  const original = new Map([...document.querySelectorAll("[data-i18n]")].map((el) => [el, el.textContent]));
@@ -118,7 +118,7 @@ function translate() {
118
118
  $("editor-title").textContent = state.id || t("newProvider");
119
119
  if (noticeTranslation) $("notice").textContent = t(noticeTranslation);
120
120
  if (editingLimits) renderLimitEditor();
121
- renderProviders(); renderModels();
121
+ renderProviders(); renderDefaultProviderNote(); renderModels();
122
122
  }
123
123
 
124
124
  async function api(path, data, signal) {
@@ -173,13 +173,25 @@ function renderProviders() {
173
173
  button.className = state.id === provider.id ? "active" : "";
174
174
  const title = document.createElement("strong"); title.textContent = provider.id;
175
175
  const detail = document.createElement("small"); detail.textContent = `${provider.models.length} models · ${provider.api || "Pi managed"}`;
176
- button.append(title, detail);
176
+ button.append(title);
177
+ if (provider.id === state.config?.defaultProvider) {
178
+ const badge = document.createElement("span"); badge.className = "provider-default-badge"; badge.textContent = t("defaultProviderBadge"); button.append(badge);
179
+ }
180
+ button.append(detail);
177
181
  button.onclick = () => { if (!state.dirty || confirm(t("unsaved"))) loadProvider(provider.id); };
178
182
  list.append(button);
179
183
  }
180
184
  setDisabled();
181
185
  }
182
186
 
187
+ function renderDefaultProviderNote() {
188
+ const isDefault = !!state.id && state.id === state.config?.defaultProvider;
189
+ const note = $("default-provider-note");
190
+ note.hidden = !isDefault;
191
+ note.textContent = isDefault ? t("defaultProviderNote") : "";
192
+ $("delete-provider").classList.toggle("delete-default-provider", isDefault);
193
+ }
194
+
183
195
  function loadProvider(id = null, preserveResults = false) {
184
196
  const previous = preserveResults ? new Map(state.models.map((model) => [model.id, model])) : new Map();
185
197
  if (!preserveResults) state.results.clear();
@@ -205,6 +217,7 @@ function loadProvider(id = null, preserveResults = false) {
205
217
  $("readonly-note").hidden = !state.readOnly;
206
218
  $("advanced-note").hidden = !provider?.hasHiddenSettings;
207
219
  $("delete-provider").hidden = !provider;
220
+ renderDefaultProviderNote();
208
221
  $("model-search").value = "";
209
222
  $("diagnostics").hidden = true;
210
223
  translate(); preview();
@@ -556,7 +569,7 @@ async function testModel(model, checks) {
556
569
  }
557
570
  async function setDefault(model) {
558
571
  if (!state.id || state.dirty || !model.selected) { notify(t("saveFirst"), "warning"); return; }
559
- await operation(async () => { const result = await api("default", { id: state.id, modelId: model.id }); state.config = result.state; renderModels(); notify(t("defaultSaved")); });
572
+ await operation(async () => { const result = await api("default", { id: state.id, modelId: model.id }); state.config = result.state; renderProviders(); renderDefaultProviderNote(); renderModels(); notify(t("defaultSaved")); });
560
573
  }
561
574
  $("provider-form").onsubmit = (event) => {
562
575
  event.preventDefault();
@@ -566,7 +579,13 @@ $("provider-form").onsubmit = (event) => {
566
579
  });
567
580
  };
568
581
  $("delete-provider").onclick = () => {
569
- if (!state.id || !confirm(t("confirmDelete"))) return;
582
+ if (!state.id) return;
583
+ if (state.id === state.config?.defaultProvider) {
584
+ notify(t("defaultProviderNote"), "warning");
585
+ $("default-provider-note").scrollIntoView({ behavior: "smooth", block: "center" });
586
+ return;
587
+ }
588
+ if (!confirm(t("confirmDelete"))) return;
570
589
  operation(async () => { const result = await api("delete", { id: state.id, revision: state.config.revision }); state.config = result.state; loadProvider(); notify(result.message); });
571
590
  };
572
591
  $("remove-key").onclick = () => {
@@ -584,6 +603,6 @@ translate();
584
603
  if (!token) notify(t("missingToken"), "error");
585
604
  else operation(async () => {
586
605
  state.config = await api("state");
587
- $("config-path").textContent = state.config.dir; $("version").textContent = `Pi ${state.config.piVersion} · Manager 0.1.0`;
606
+ $("config-path").textContent = state.config.dir; $("version").textContent = `Pi ${state.config.piVersion} · Manager ${state.config.managerVersion}`;
588
607
  loadProvider(state.config.providers.find((p) => !p.readOnly)?.id || null);
589
608
  });
package/web/index.html CHANGED
@@ -27,6 +27,7 @@
27
27
  <section class="card">
28
28
  <div class="section-heading"><div><span class="step">01</span><h2 data-i18n="connection">Connection</h2></div><span class="tag" data-i18n="native">Pi native</span></div>
29
29
  <div id="readonly-note" class="info" hidden data-i18n="readonly">This provider is managed by Pi or another integration. Create a custom provider to edit here.</div>
30
+ <div id="default-provider-note" class="warning" role="status" hidden></div>
30
31
  <fieldset id="connection-fields">
31
32
  <div class="grid two">
32
33
  <label><span data-i18n="providerId">Provider ID</span><input id="provider-id" required maxlength="80" placeholder="my-gateway" autocomplete="off" spellcheck="false"><small data-i18n="idHelp">A unique name used by Pi, e.g. my-gateway.</small></label>
@@ -48,13 +49,14 @@
48
49
  <section class="card">
49
50
  <div class="section-heading"><div><span class="step">02</span><h2 data-i18n="models">Models</h2><span id="model-count" class="count">0</span></div><div class="action-row"><button id="fill-limits" type="button" class="quiet" data-i18n="fillLimits">Fill missing limits</button><button id="add-model" type="button" class="quiet" data-i18n="addModel">+ Manual model</button></div></div>
50
51
  <p class="hint" data-i18n="modelHint">Select models to save. A listed ID does not guarantee chat, vision or tool support.</p>
52
+ <p class="hint" data-i18n="defaultActionHelp">“Set default” in a model's Actions makes that model and provider Pi's default for new launches.</p>
51
53
  <p class="hint" data-i18n="limitsHelp">Context/output defaults come from endpoint metadata, then exact, unambiguous matches in Pi's bundled catalog. Gateway limits can differ. Existing values are kept; save the provider to persist filled defaults.</p>
52
54
  <p class="hint" data-i18n="probeHelp">Test chat sends one short request. Check capabilities runs chat, a two-turn test tool, reasoning and a generated image (up to 5 requests). Uses API tokens: up to 256 output tokens per request, 2,048 for reasoning. Results appear below each model.</p>
53
55
  <div class="model-toolbar"><input id="model-search" type="search" placeholder="Search model IDs…" aria-label="Search models"><label class="checkbox-line"><input id="select-all" type="checkbox"><span data-i18n="selectAll">Select visible</span></label><span id="selected-count" class="hint">0 selected</span></div>
54
56
  <div class="table-scroll"><table><thead><tr><th class="check-col"></th><th data-i18n="modelId">Model ID</th><th data-i18n="tokenLimits">Token limits</th><th data-i18n="input">Input</th><th data-i18n="actions">Actions</th></tr></thead><tbody id="models-body"></tbody></table></div>
55
57
  <div id="empty-models" class="empty"><span class="empty-icon">◇</span><h3 data-i18n="noModels">Connect your first model</h3><p data-i18n="noModelsHint">Fetch models from your endpoint, or add a model ID manually.</p></div>
56
58
  </section>
57
- <div class="save-bar"><div><strong id="save-summary"></strong><small data-i18n="saveHelp">Models → models.json · Key → auth.json</small></div><div class="action-row"><button id="delete-provider" type="button" class="text-danger" hidden data-i18n="deleteProvider">Delete provider</button><button id="save-provider" type="submit" class="primary" data-i18n="save">Save provider</button></div></div>
59
+ <div class="save-bar"><div><strong id="save-summary"></strong><small data-i18n="saveHelp">Models → models.json · Key → auth.json</small></div><div class="action-row"><button id="delete-provider" type="button" class="text-danger" hidden aria-describedby="default-provider-note" data-i18n="deleteProvider">Delete provider</button><button id="save-provider" type="submit" class="primary" data-i18n="save">Save provider</button></div></div>
58
60
  </form>
59
61
  <p class="footnote" data-i18n="footnote">Configuration is saved locally. Open /model in Pi to select a saved model. “Set default” applies to new Pi launches.</p>
60
62
  </main>
package/web/style.css CHANGED
@@ -93,6 +93,7 @@ nav button { text-align: left; border-color: transparent; background: transparen
93
93
  nav button.active { background: var(--soft); color: var(--accent); border-color: #c9e0d8; }
94
94
  nav button strong { display: block; overflow-wrap: anywhere; }
95
95
  nav button small { display: block; color: var(--muted); margin-top: 6px; overflow-wrap: anywhere; line-height: 1.6; }
96
+ .provider-default-badge { display: inline-block; margin-top: 8px; border-radius: 6px; background: #e4f4eb; color: #21634e; padding: 4px 9px; font-weight: 600; }
96
97
 
97
98
  .sidebar-footer {
98
99
  margin-top: auto;
@@ -214,7 +215,7 @@ td button { padding: 9px 10px; }
214
215
  .save-bar small { display: block; color: var(--muted); margin-top: 8px; }
215
216
  .footnote { color: var(--muted); line-height: 1.7; margin-top: 24px; }
216
217
 
217
- .diagnostics, #notice, .info {
218
+ .diagnostics, #notice, .info, .warning {
218
219
  padding: 16px 18px;
219
220
  border-radius: 8px;
220
221
  line-height: 1.7;
@@ -226,8 +227,9 @@ td button { padding: 9px 10px; }
226
227
  }
227
228
 
228
229
  .diagnostics.error, #notice.error { background: #fff1ef; color: #9f3e3e; }
229
- #notice, .info { margin: 0 0 24px; }
230
- .diagnostics.warning, #notice.warning { background: #fff6df; color: #816522; }
230
+ #notice, .info, .warning { margin: 0 0 24px; }
231
+ .diagnostics.warning, #notice.warning, .warning { background: #fff6df; color: #765718; }
232
+ .delete-default-provider { border: 1px solid #e8caca; padding: 8px 12px; }
231
233
 
232
234
  dialog {
233
235
  border: 1px solid var(--line);