dsh-any-deepseek 0.0.18 โ 0.0.19
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/lib/client.js +87 -43
- package/lib/index.js +79 -6
- package/lib/usage-ledger.d.ts +29 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -161,6 +161,19 @@ window.__ModuleLoader__.load({
|
|
|
161
161
|
return `${(value / 1e6).toFixed(value < 1e7 ? 2 : 1)}M`;
|
|
162
162
|
}
|
|
163
163
|
/**
|
|
164
|
+
* Format a mean-context figure, or an em dash when nothing reported usage.
|
|
165
|
+
*
|
|
166
|
+
* ๐ด Same discipline as {@link formatSpeed}: `null` is not `0`. "No request reported usage" and
|
|
167
|
+
* "every request carried zero tokens" are different statements, and only the first one is true
|
|
168
|
+
* here โ so the unmeasurable case renders as an em dash.
|
|
169
|
+
* @param value - mean tokens per request, or null/undefined.
|
|
170
|
+
* @returns e.g. `28.1K`, or `โ`.
|
|
171
|
+
*/
|
|
172
|
+
function formatMeanContext(value) {
|
|
173
|
+
if (value === null || value === void 0 || !Number.isFinite(value) || value <= 0) return "โ";
|
|
174
|
+
return formatTokens(Math.round(value));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
164
177
|
* Format a throughput figure, or an em dash when it is unmeasurable.
|
|
165
178
|
*
|
|
166
179
|
* ๐ด `null` is NOT zero: a group with no first-token measurement has no speed, and printing `0`
|
|
@@ -188,7 +201,7 @@ window.__ModuleLoader__.load({
|
|
|
188
201
|
}
|
|
189
202
|
//#endregion
|
|
190
203
|
//#region \0ad-css:src/client/SettingsPage
|
|
191
|
-
const css = "/* Panel styles.\n *\n * ๐ด There are NO `--dsw-alias-*` design tokens on this host (measured 2026-10-21: the document\n * declares 29 custom properties and not one is a colour). This file used to reference a dozen of\n * them, every one falling back to a hardcoded LIGHT value (`rgba(0,0,0,0.12)` and friends) โ so\n * on the dark theme the borders and dividers were effectively invisible.\n *\n * โ Derive everything from `currentColor`, which the host sets per theme. `color-mix` gives the\n * low-contrast borders and fills that a token would have provided, and it follows the theme\n * automatically instead of assuming one palette.\n */\n\n.ad-page {\n display: flex;\n flex-direction: column;\n gap: 20px;\n padding: 4px 2px 12px;\n}\n\n/* One place to change the two theme-derived values the whole panel uses. */\n.ad-page {\n --ad-line: color-mix(in srgb, currentColor 18%, transparent);\n --ad-line-strong: color-mix(in srgb, currentColor 32%, transparent);\n --ad-fill-subtle: color-mix(in srgb, currentColor 7%, transparent);\n /* The current row's fill: visible against both themes because it is derived from the text\n colour the host already picked, not from an assumed palette. */\n --ad-fill-current: color-mix(in srgb, currentColor 11%, transparent);\n --ad-danger: #e5534b;\n}\n\n.ad-head {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.ad-title {\n margin: 0;\n font-size: 15px;\n font-weight: 600;\n}\n\n.ad-intro {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n opacity: 0.72;\n}\n\n.ad-error {\n border: 1px solid var(--ad-danger);\n border-radius: 6px;\n padding: 8px 10px;\n font-size: 12px;\n}\n\n.ad-section {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.ad-sectionTitle {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n opacity: 0.6;\n}\n\n.ad-rows {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.ad-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n border: 1px solid var(--ad-line);\n border-radius: 8px;\n padding: 10px 12px;\n /*\n * The left edge is reserved on EVERY row so the current one does not shift its content when\n * selected โ a row that reindents on click makes the list feel like it moved.\n */\n border-left: 3px solid transparent;\n}\n\n/*\n * ๐ด The current channel is marked three ways, not one.\n *\n * It used to be a small badge plus a barely-different border, and the owner could not tell at a\n * glance which relay was live โ glanceability is the whole point of a single-choice list. So:\n * a colour bar down the left edge, a stronger border, and a tinted fill. All derived from\n * `currentColor`, because this host ships no colour tokens (measured: 29 custom properties, none\n * of them a colour), so any hardcoded value would assume the wrong theme.\n */\n.ad-row-current {\n border-color: var(--ad-line-strong);\n border-left-color: currentColor;\n background: var(--ad-fill-current);\n}\n\n/* The ๅฝๅ badge keeps its place as the textual confirmation, for when colour is not enough. */\n.ad-row-current .ad-badge {\n border-color: currentColor;\n opacity: 1;\n font-weight: 600;\n}\n\n/* A rename button must not look like the row's own selection affordance. */\n.ad-renameBtn {\n margin-left: auto;\n flex: none;\n}\n\n.ad-rowMain {\n display: flex;\n flex-direction: column;\n gap: 4px;\n min-width: 0;\n}\n\n.ad-rowTitleLine {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n font-weight: 500;\n}\n\n.ad-rowTitle {\n white-space: nowrap;\n}\n\n.ad-badge {\n font-size: 10px;\n padding: 1px 6px;\n border-radius: 999px;\n border: 1px solid currentColor;\n opacity: 0.8;\n}\n\n.ad-warn {\n color: #b7791f;\n cursor: help;\n}\n\n.ad-rowMeta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n opacity: 0.66;\n min-width: 0;\n}\n\n.ad-mono {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n word-break: break-all;\n}\n\n.ad-sep {\n opacity: 0.5;\n}\n\n.ad-rowActions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n}\n\n/*\n * Buttons: sized to be hit, not guessed at.\n *\n * The previous 4px/10px padding made a 22px-tall target, which is below the comfortable\n * threshold and made the row actions (ๆต่ฏ / ็งป้ค) look like text rather than controls. 6px/12px\n * with a minimum width gives each one a real box while keeping the row compact โ a row action is\n * secondary, so it should be easy to hit and easy to ignore.\n */\n.ad-btn {\n font-size: 11px;\n font-family: inherit;\n line-height: 1.5;\n padding: 5px 12px;\n min-width: 56px;\n min-height: 26px;\n border-radius: 6px;\n border: 1px solid var(--ad-line);\n background: transparent;\n color: inherit;\n cursor: pointer;\n transition: background-color 120ms ease, border-color 120ms ease, opacity 120ms ease;\n}\n\n.ad-btn:hover:not(:disabled) {\n border-color: var(--ad-line-strong);\n background: var(--ad-fill-subtle);\n}\n\n.ad-btn:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.ad-btn-quiet {\n opacity: 0.85;\n}\n\n.ad-probe {\n font-size: 11px;\n padding-top: 2px;\n}\n\n.ad-probe-ok {\n color: #2f855a;\n}\n\n.ad-probe-bad {\n color: #c53030;\n word-break: break-word;\n}\n\n.ad-hint,\n.ad-empty,\n.ad-note {\n margin: 0;\n font-size: 11px;\n line-height: 1.6;\n opacity: 0.66;\n}\n\n.ad-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n display: inline-block;\n}\n\n.ad-dot-on {\n background: #2f855a;\n}\n\n.ad-dot-off {\n border: 1px solid currentColor;\n opacity: 0.5;\n}\n\n.ad-dot-unknown {\n background: linear-gradient(90deg, currentColor 50%, transparent 50%);\n opacity: 0.5;\n}\n\n/* The channel row is a radio \"pick\" zone: the whole left side is clickable, and the radio sits\n at the start of the line so \"which one is live\" is readable down a single column. */\n.ad-rowPick {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n flex: 1;\n min-width: 0;\n cursor: pointer;\n}\n\n.ad-rowPick input[type='radio'] {\n flex: none;\n /* Optically centred against the first text line rather than the row box. */\n margin-top: 3px;\n accent-color: var(--ad-line-strong);\n cursor: pointer;\n}\n\n/* The switch is gone: selection is the only axis (see `channels.ts`). Keeping the rules would\n invite someone to bring the second axis back. */\n\n/* โโ top-level tabs (ๆธ ้ / ็จ้) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\n\n/*\n * The panel answers two questions asked at different moments: \"which relay serves me and does\n * its key work\" and \"what has each relay cost\". They used to be stacked on one scroll, so\n * reading the cost table meant scrolling past every channel row. As tabs, each answer gets the\n * whole panel and the reader never scrolls to reach the second one.\n *\n * The selected tab is marked by a bottom rule plus full opacity โ not by colour alone, because\n * this host ships no colour tokens and a colour-only cue would vary with the theme.\n */\n.ad-tabs {\n display: flex;\n gap: 4px;\n border-bottom: 1px solid var(--ad-line);\n}\n\n.ad-tab {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Comfortable click target: 28px tall is enough to hit without aiming, and still compact\n enough that two tabs do not dominate the panel. */\n padding: 6px 14px;\n font-size: 12px;\n font-family: inherit;\n border: none;\n border-bottom: 2px solid transparent;\n /* Pulled down over the container's rule so the selected tab's underline replaces it. */\n margin-bottom: -1px;\n background: transparent;\n color: inherit;\n opacity: 0.62;\n cursor: pointer;\n}\n\n.ad-tab:hover {\n opacity: 0.85;\n}\n\n.ad-tab-on {\n opacity: 1;\n font-weight: 600;\n border-bottom-color: currentColor;\n}\n\n/* A count on the channel tab, so \"how many do I have\" is answered without opening it. */\n.ad-tabCount {\n font-size: 10px;\n font-weight: 500;\n min-width: 16px;\n padding: 0 4px;\n border-radius: 999px;\n text-align: center;\n background: var(--ad-fill-subtle);\n opacity: 0.8;\n}\n\n/* โโ segmented control (ๆๆธ ้ / ๆๅคฉ / ๅทๆฐ) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\n\n/*\n * A segmented control rather than free-floating pill buttons: these three share one meaning\n * (\"how the table below is grouped, and re-read it\"), so they are drawn as one object. The\n * borders collapse to avoid double lines between neighbours.\n */\n.ad-segmented {\n display: inline-flex;\n align-items: stretch;\n border: 1px solid var(--ad-line);\n border-radius: 7px;\n overflow: hidden;\n}\n\n.ad-seg {\n padding: 4px 11px;\n font-size: 11px;\n font-family: inherit;\n border: none;\n border-right: 1px solid var(--ad-line);\n background: transparent;\n color: inherit;\n opacity: 0.7;\n cursor: pointer;\n}\n\n.ad-seg:last-child {\n border-right: none;\n}\n\n.ad-seg:hover:not(:disabled) {\n opacity: 1;\n background: var(--ad-fill-subtle);\n}\n\n.ad-seg-on {\n opacity: 1;\n font-weight: 600;\n background: var(--ad-fill-subtle);\n}\n\n/* Refresh is an action, not a grouping โ a gap separates it from the choice it refreshes. */\n.ad-seg-refresh {\n border-left: 1px solid var(--ad-line);\n}\n\n.ad-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 11px;\n}\n\n.ad-table th,\n.ad-table td {\n padding: 5px 8px;\n border-bottom: 1px solid var(--ad-line);\n text-align: left;\n}\n\n.ad-table th {\n font-weight: 500;\n opacity: 0.6;\n}\n\n.ad-num {\n text-align: right;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n\n.ad-total td {\n font-weight: 600;\n border-bottom: none;\n}\n\n/* โโ add-channel form โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\n\n.ad-addForm {\n display: flex;\n flex-direction: column;\n gap: 14px;\n border: 1px dashed var(--ad-line);\n border-radius: 8px;\n padding: 14px;\n}\n\n.ad-field {\n display: flex;\n flex-direction: column;\n gap: 5px;\n font-size: 11px;\n}\n\n/* The label row: the field name, plus a required/optional marker that says which is which. */\n.ad-field > span:first-child {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n/*\n * ๅฟ
้ / ๅฏ็ฉบ as a small marker rather than a sentence.\n *\n * The add form used to leave the reader guessing which fields could be skipped โ and the one\n * field that could not be skipped (the key) looked exactly like the two that could. The marker\n * is text, not colour, so it survives both themes and any colour-blindness.\n */\n.ad-req,\n.ad-opt {\n font-style: normal;\n font-size: 10px;\n font-weight: 400;\n padding: 1px 5px;\n border-radius: 4px;\n opacity: 0.7;\n}\n\n.ad-req {\n border: 1px solid var(--ad-line-strong);\n}\n\n.ad-opt {\n border: 1px dotted var(--ad-line);\n}\n\n/* Why this field exists, in one line โ printed under the control, not hidden in a tooltip. */\n.ad-fieldHelp {\n font-size: 11px;\n line-height: 1.55;\n opacity: 0.6;\n}\n\n.ad-input {\n font-size: 12px;\n font-family: inherit;\n /* 6px/9px keeps the control the same height as a `.ad-btn` (26px), so a field and its button\n on one line align without a per-case override. */\n padding: 5px 9px;\n min-height: 26px;\n border-radius: 6px;\n border: 1px solid var(--ad-line);\n background: transparent;\n color: inherit;\n}\n\n.ad-input:focus-visible {\n outline: 2px solid var(--ad-line-strong);\n outline-offset: 1px;\n border-color: var(--ad-line-strong);\n}\n\n/*\n * ๐ด A native `<select>` cannot be themed by leaving the background transparent.\n *\n * The popup list is drawn by the browser, not by the closed control's box: the options inherit\n * nothing from it, so `background: transparent` made them render in the platform default (light)\n * palette against this dark theme โ reported by the owner and measured on the real host.\n *\n * ๐ด SECOND correction (measured 2026-10-21): this file used `var(--dsw-alias-color-*, #fallback)`\n * throughout, and **none of those variables exist**. The host declares only 29 custom properties\n * in total and not one is a colour token, so every `var()` here was falling back โ and a fallback\n * that happens to be set inside a shorthand can be silently dropped (the select's background\n * computed to `rgba(0,0,0,0)`, i.e. still transparent, which is exactly the bug).\n *\n * โ Use only values this file controls, derived from `currentColor` (the host sets a light text\n * colour on both themes) plus explicit translucent layers. No host token is assumed.\n */\nselect.ad-input {\n /* Tells the engine which palette to use for the popup. This part DID work. */\n color-scheme: dark light;\n /*\n * Paint the closed box with a layer that follows the surrounding text colour, so it matches\n * whichever theme is active instead of hardcoding a dark value into a light theme.\n */\n background-color: color-mix(in srgb, currentColor 8%, transparent);\n color: inherit;\n cursor: pointer;\n padding-right: 22px;\n /* Room for the chevron drawn below, so text never runs under it. */\n appearance: none;\n background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),\n linear-gradient(135deg, currentColor 50%, transparent 50%);\n background-position: calc(100% - 12px) calc(50% + 1px), calc(100% - 8px) calc(50% + 1px);\n background-size: 4px 4px, 4px 4px;\n background-repeat: no-repeat;\n}\n\nselect.ad-input:hover:not(:disabled) {\n background-color: color-mix(in srgb, currentColor 14%, transparent);\n}\n\nselect.ad-input:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 1px;\n}\n\n/*\n * The popup itself follows `color-scheme`; these keep the option rows explicit for engines that\n * honour per-option colours. `color-scheme: dark` above is what actually stops the light-on-dark\n * list, because the popup is native chrome rather than a styled element.\n */\nselect.ad-input option {\n background-color: Canvas;\n color: CanvasText;\n}\n\n.ad-fieldError {\n margin: 0;\n font-size: 11px;\n color: #c53030;\n}\n\n/* โโ key row โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\n\n/*\n * The key line states a FACT and offers the fix beside it: \"configured / not configured\",\n * the reference name the secret is filed under, and one text button that opens the input.\n *\n * The value is never shown โ the harness credential seam answers \"configured?\" and refuses to\n * answer anything else, so there is nothing to render even if we wanted to.\n */\n.ad-keyLine {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n padding-top: 2px;\n}\n\n.ad-keyOn {\n color: #2f855a;\n}\n\n.ad-keyOff {\n color: #b7791f;\n}\n\n/*\n * Remaining balance.\n *\n * Same line shape as the key state โ it is another fact about this channel, not a control. The\n * subtle fill marks it as a measurement (like `ad-keyName`) rather than a warning: the words\n * themselves say whether it is fine (\"72.50 USD\") or not (\"๏ผไฝ้ขไธ่ถณ๏ผ\").\n */\n.ad-balance {\n font-variant-numeric: tabular-nums;\n font-size: 11px;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--ad-fill-subtle);\n}\n\n/* A section heading with one action on its right (currently: ๆฅ่ฏขไฝ้ข). */\n.ad-sectionBar {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n}\n\n/* The reference name, not a secret: monospace so it lines up and misreads as a name, not a value. */\n.ad-keyName {\n font-size: 10px;\n padding: 1px 5px;\n border-radius: 4px;\n background: var(--ad-fill-subtle);\n opacity: 0.8;\n}\n\n/* A text-shaped action. Styled as a link because it opens an inline editor, not a dialog. */\n.ad-link {\n font-size: 11px;\n padding: 0;\n border: none;\n background: none;\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n opacity: 0.75;\n cursor: pointer;\n}\n\n.ad-link:hover {\n opacity: 1;\n}\n\n/* The inline key editor: one masked field and one button, on the same line as the fact above. */\n.ad-keyForm {\n display: flex;\n align-items: center;\n gap: 6px;\n padding-top: 4px;\n}\n\n.ad-keyForm .ad-input {\n flex: 1;\n min-width: 0;\n}\n\n/* A destructive action is quiet until hovered: it should be findable, not inviting. */\n.ad-btn-danger {\n opacity: 0.6;\n}\n\n.ad-btn-danger:hover:not(:disabled) {\n opacity: 1;\n border-color: var(--ad-danger);\n color: var(--ad-danger);\n}\n\n/*\n * The one hint that must not be skimmed past: where the key ends up.\n *\n * It carries a left rule and slightly higher opacity than an ordinary hint, because a reader who\n * skips it has been told something false by omission โ the key IS in the profile document.\n */\n.ad-hint-warn {\n padding: 8px 10px;\n border-left: 3px solid var(--ad-line-strong);\n background: var(--ad-fill-subtle);\n border-radius: 0 6px 6px 0;\n opacity: 0.82;\n}\n\n/*\n * The table can be wider than a narrow settings pane (ten columns of numbers). Scrolling it\n * inside its own box keeps the tab strip and the channel list reachable, instead of pushing the\n * whole panel sideways.\n */\n.ad-tableWrap {\n overflow-x: auto;\n /* A hint that there is more to the right, without a scrollbar competing with the borders. */\n scrollbar-width: thin;\n}\n\n.ad-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 11px;\n}\n\n/* The first column is a name, not a number: keep it from being squeezed by the numeric columns. */\n.ad-table th:first-child,\n.ad-table td:first-child {\n white-space: nowrap;\n padding-right: 14px;\n}\n\n/* โโ footer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\n\n/*\n * States what this thing is (MIT, open, improvable) and where it lives. A user deciding whether\n * to fork it or add a relay should not have to search for the source.\n */\n.ad-foot {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 8px;\n padding-top: 10px;\n border-top: 1px solid var(--ad-line);\n font-size: 11px;\n opacity: 0.6;\n}\n\n.ad-footLink {\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 10px;\n}\n\n.ad-footLink:hover {\n opacity: 0.9;\n}\n";
|
|
204
|
+
const css = "/* Panel styles.\n *\n * ๐ด There are NO `--dsw-alias-*` design tokens on this host (measured 2026-10-21: the document\n * declares 29 custom properties and not one is a colour). This file used to reference a dozen of\n * them, every one falling back to a hardcoded LIGHT value (`rgba(0,0,0,0.12)` and friends) โ so\n * on the dark theme the borders and dividers were effectively invisible.\n *\n * โ Derive everything from `currentColor`, which the host sets per theme. `color-mix` gives the\n * low-contrast borders and fills that a token would have provided, and it follows the theme\n * automatically instead of assuming one palette.\n */\n\n.ad-page {\n display: flex;\n flex-direction: column;\n gap: 20px;\n padding: 4px 2px 12px;\n}\n\n/* One place to change the two theme-derived values the whole panel uses. */\n.ad-page {\n --ad-line: color-mix(in srgb, currentColor 18%, transparent);\n --ad-line-strong: color-mix(in srgb, currentColor 32%, transparent);\n --ad-fill-subtle: color-mix(in srgb, currentColor 7%, transparent);\n /* The current row's fill: visible against both themes because it is derived from the text\n colour the host already picked, not from an assumed palette. */\n --ad-fill-current: color-mix(in srgb, currentColor 11%, transparent);\n --ad-danger: #e5534b;\n}\n\n.ad-head {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.ad-title {\n margin: 0;\n font-size: 15px;\n font-weight: 600;\n}\n\n.ad-intro {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n opacity: 0.72;\n}\n\n.ad-error {\n border: 1px solid var(--ad-danger);\n border-radius: 6px;\n padding: 8px 10px;\n font-size: 12px;\n}\n\n.ad-section {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.ad-sectionTitle {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n opacity: 0.6;\n}\n\n.ad-rows {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n/*\n * ๐ด Compact rows (owner request 2026-10-24: ใๆฏไธชๆธ ้้ซๅบฆๅธๆๅฐไธไบ๏ผ็ดงๅไบใ).\n *\n * The row used to be a four-line stack โ title / modelยทapiยทbaseURL / key line / balance โ and\n * with five channels that is most of the panel spent on air. The fix is layout, not information:\n * everything stays on screen, but the secondary facts move BESIDE the name instead of under it.\n *\n * What changed and why:\n * ยท padding 10pxโ7px and gap 6pxโ4px: the row now reads as one line of text plus a hairline of\n * metadata, instead of a card with its own interior spacing;\n * ยท `ad-rowMain` became a ROW (was a column): the title line and the meta line share one axis,\n * so the row is two text lines tall at most, not four;\n * ยท the meta line keeps `flex-wrap` โ on a narrow panel it may wrap, and that is the correct\n * trade: a row that stays readable beats one that clips the endpoint URL.\n */\n.ad-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n border: 1px solid var(--ad-line);\n border-radius: 8px;\n padding: 7px 10px;\n /*\n * The left edge is reserved on EVERY row so the current one does not shift its content when\n * selected โ a row that reindents on click makes the list feel like it moved.\n */\n border-left: 3px solid transparent;\n}\n\n/*\n * ๐ด The current channel is marked three ways, not one.\n *\n * It used to be a small badge plus a barely-different border, and the owner could not tell at a\n * glance which relay was live โ glanceability is the whole point of a single-choice list. So:\n * a colour bar down the left edge, a stronger border, and a tinted fill. All derived from\n * `currentColor`, because this host ships no colour tokens (measured: 29 custom properties, none\n * of them a colour), so any hardcoded value would assume the wrong theme.\n */\n.ad-row-current {\n border-color: var(--ad-line-strong);\n border-left-color: currentColor;\n background: var(--ad-fill-current);\n}\n\n/* The ๅฝๅ badge keeps its place as the textual confirmation, for when colour is not enough. */\n.ad-row-current .ad-badge {\n border-color: currentColor;\n opacity: 1;\n font-weight: 600;\n}\n\n/* A rename button must not look like the row's own selection affordance. */\n.ad-renameBtn {\n margin-left: auto;\n flex: none;\n}\n\n/*\n * The row's content is a COLUMN of at most two lines: (1) name + status, (2) everything else.\n * It was a four-line stack before the compactness pass โ see the note on `.ad-row`.\n */\n.ad-rowMain {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n flex: 1 1 auto;\n}\n\n.ad-rowTitleLine {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n font-weight: 500;\n min-width: 0;\n}\n\n.ad-rowTitle {\n white-space: nowrap;\n}\n\n.ad-badge {\n font-size: 10px;\n padding: 1px 6px;\n border-radius: 999px;\n border: 1px solid currentColor;\n opacity: 0.8;\n}\n\n.ad-warn {\n color: #b7791f;\n cursor: help;\n}\n\n/*\n * The single detail line under a channel's name.\n *\n * It carries what used to be FOUR stacked lines (endpoint identity / key state / probe verdict /\n * balance). `flex-wrap` is deliberate: on a narrow panel these wrap to a second line, which is a\n * better outcome than clipping the endpoint URL. `row-gap` is tighter than `column-gap` so a wrap\n * still reads as one paragraph rather than as separate rows.\n */\n.ad-rowDetail {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n column-gap: 6px;\n row-gap: 2px;\n font-size: 11px;\n opacity: 0.66;\n min-width: 0;\n}\n\n.ad-mono {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n word-break: break-all;\n}\n\n.ad-sep {\n opacity: 0.5;\n}\n\n.ad-rowActions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n}\n\n/*\n * Buttons: sized to be hit, not guessed at.\n *\n * The previous 4px/10px padding made a 22px-tall target, which is below the comfortable\n * threshold and made the row actions (ๆต่ฏ / ็งป้ค) look like text rather than controls. 6px/12px\n * with a minimum width gives each one a real box while keeping the row compact โ a row action is\n * secondary, so it should be easy to hit and easy to ignore.\n */\n.ad-btn {\n font-size: 11px;\n font-family: inherit;\n line-height: 1.5;\n padding: 5px 12px;\n min-width: 56px;\n min-height: 26px;\n border-radius: 6px;\n border: 1px solid var(--ad-line);\n background: transparent;\n color: inherit;\n cursor: pointer;\n transition: background-color 120ms ease, border-color 120ms ease, opacity 120ms ease;\n}\n\n.ad-btn:hover:not(:disabled) {\n border-color: var(--ad-line-strong);\n background: var(--ad-fill-subtle);\n}\n\n.ad-btn:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.ad-btn-quiet {\n opacity: 0.85;\n}\n\n.ad-probe {\n font-size: 11px;\n padding-top: 2px;\n}\n\n.ad-probe-ok {\n color: #2f855a;\n}\n\n.ad-probe-bad {\n color: #c53030;\n word-break: break-word;\n}\n\n.ad-hint,\n.ad-empty,\n.ad-note {\n margin: 0;\n font-size: 11px;\n line-height: 1.6;\n opacity: 0.66;\n}\n\n.ad-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n display: inline-block;\n}\n\n.ad-dot-on {\n background: #2f855a;\n}\n\n.ad-dot-off {\n border: 1px solid currentColor;\n opacity: 0.5;\n}\n\n.ad-dot-unknown {\n background: linear-gradient(90deg, currentColor 50%, transparent 50%);\n opacity: 0.5;\n}\n\n/* The channel row is a radio \"pick\" zone: the whole left side is clickable, and the radio sits\n at the start of the line so \"which one is live\" is readable down a single column. */\n.ad-rowPick {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n flex: 1;\n min-width: 0;\n cursor: pointer;\n}\n\n.ad-rowPick input[type='radio'] {\n flex: none;\n /* Optically centred against the first text line rather than the row box. */\n margin-top: 3px;\n accent-color: var(--ad-line-strong);\n cursor: pointer;\n}\n\n/* The switch is gone: selection is the only axis (see `channels.ts`). Keeping the rules would\n invite someone to bring the second axis back. */\n\n/* โโ top-level tabs (ๆธ ้ / ็จ้) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\n\n/*\n * The panel answers two questions asked at different moments: \"which relay serves me and does\n * its key work\" and \"what has each relay cost\". They used to be stacked on one scroll, so\n * reading the cost table meant scrolling past every channel row. As tabs, each answer gets the\n * whole panel and the reader never scrolls to reach the second one.\n *\n * The selected tab is marked by a bottom rule plus full opacity โ not by colour alone, because\n * this host ships no colour tokens and a colour-only cue would vary with the theme.\n */\n.ad-tabs {\n display: flex;\n gap: 4px;\n border-bottom: 1px solid var(--ad-line);\n}\n\n.ad-tab {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Comfortable click target: 28px tall is enough to hit without aiming, and still compact\n enough that two tabs do not dominate the panel. */\n padding: 6px 14px;\n font-size: 12px;\n font-family: inherit;\n border: none;\n border-bottom: 2px solid transparent;\n /* Pulled down over the container's rule so the selected tab's underline replaces it. */\n margin-bottom: -1px;\n background: transparent;\n color: inherit;\n opacity: 0.62;\n cursor: pointer;\n}\n\n.ad-tab:hover {\n opacity: 0.85;\n}\n\n.ad-tab-on {\n opacity: 1;\n font-weight: 600;\n border-bottom-color: currentColor;\n}\n\n/* A count on the channel tab, so \"how many do I have\" is answered without opening it. */\n.ad-tabCount {\n font-size: 10px;\n font-weight: 500;\n min-width: 16px;\n padding: 0 4px;\n border-radius: 999px;\n text-align: center;\n background: var(--ad-fill-subtle);\n opacity: 0.8;\n}\n\n/* โโ segmented control (ๆๆธ ้ / ๆๅคฉ / ๅทๆฐ) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\n\n/*\n * A segmented control rather than free-floating pill buttons: these three share one meaning\n * (\"how the table below is grouped, and re-read it\"), so they are drawn as one object. The\n * borders collapse to avoid double lines between neighbours.\n */\n.ad-segmented {\n display: inline-flex;\n align-items: stretch;\n border: 1px solid var(--ad-line);\n border-radius: 7px;\n overflow: hidden;\n}\n\n.ad-seg {\n padding: 4px 11px;\n font-size: 11px;\n font-family: inherit;\n border: none;\n border-right: 1px solid var(--ad-line);\n background: transparent;\n color: inherit;\n opacity: 0.7;\n cursor: pointer;\n}\n\n.ad-seg:last-child {\n border-right: none;\n}\n\n.ad-seg:hover:not(:disabled) {\n opacity: 1;\n background: var(--ad-fill-subtle);\n}\n\n.ad-seg-on {\n opacity: 1;\n font-weight: 600;\n background: var(--ad-fill-subtle);\n}\n\n/* Refresh is an action, not a grouping โ a gap separates it from the choice it refreshes. */\n.ad-seg-refresh {\n border-left: 1px solid var(--ad-line);\n}\n\n.ad-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 11px;\n}\n\n.ad-table th,\n.ad-table td {\n padding: 5px 8px;\n border-bottom: 1px solid var(--ad-line);\n text-align: left;\n}\n\n.ad-table th {\n font-weight: 500;\n opacity: 0.6;\n}\n\n.ad-num {\n text-align: right;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n\n.ad-total td {\n font-weight: 600;\n border-bottom: none;\n}\n\n/* โโ add-channel form โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\n\n.ad-addForm {\n display: flex;\n flex-direction: column;\n gap: 14px;\n border: 1px dashed var(--ad-line);\n border-radius: 8px;\n padding: 14px;\n}\n\n.ad-field {\n display: flex;\n flex-direction: column;\n gap: 5px;\n font-size: 11px;\n}\n\n/* The label row: the field name, plus a required/optional marker that says which is which. */\n.ad-field > span:first-child {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n/*\n * ๅฟ
้ / ๅฏ็ฉบ as a small marker rather than a sentence.\n *\n * The add form used to leave the reader guessing which fields could be skipped โ and the one\n * field that could not be skipped (the key) looked exactly like the two that could. The marker\n * is text, not colour, so it survives both themes and any colour-blindness.\n */\n.ad-req,\n.ad-opt {\n font-style: normal;\n font-size: 10px;\n font-weight: 400;\n padding: 1px 5px;\n border-radius: 4px;\n opacity: 0.7;\n}\n\n.ad-req {\n border: 1px solid var(--ad-line-strong);\n}\n\n.ad-opt {\n border: 1px dotted var(--ad-line);\n}\n\n/* Why this field exists, in one line โ printed under the control, not hidden in a tooltip. */\n.ad-fieldHelp {\n font-size: 11px;\n line-height: 1.55;\n opacity: 0.6;\n}\n\n.ad-input {\n font-size: 12px;\n font-family: inherit;\n /* 6px/9px keeps the control the same height as a `.ad-btn` (26px), so a field and its button\n on one line align without a per-case override. */\n padding: 5px 9px;\n min-height: 26px;\n border-radius: 6px;\n border: 1px solid var(--ad-line);\n background: transparent;\n color: inherit;\n}\n\n.ad-input:focus-visible {\n outline: 2px solid var(--ad-line-strong);\n outline-offset: 1px;\n border-color: var(--ad-line-strong);\n}\n\n/*\n * ๐ด A native `<select>` cannot be themed by leaving the background transparent.\n *\n * The popup list is drawn by the browser, not by the closed control's box: the options inherit\n * nothing from it, so `background: transparent` made them render in the platform default (light)\n * palette against this dark theme โ reported by the owner and measured on the real host.\n *\n * ๐ด SECOND correction (measured 2026-10-21): this file used `var(--dsw-alias-color-*, #fallback)`\n * throughout, and **none of those variables exist**. The host declares only 29 custom properties\n * in total and not one is a colour token, so every `var()` here was falling back โ and a fallback\n * that happens to be set inside a shorthand can be silently dropped (the select's background\n * computed to `rgba(0,0,0,0)`, i.e. still transparent, which is exactly the bug).\n *\n * โ Use only values this file controls, derived from `currentColor` (the host sets a light text\n * colour on both themes) plus explicit translucent layers. No host token is assumed.\n */\nselect.ad-input {\n /* Tells the engine which palette to use for the popup. This part DID work. */\n color-scheme: dark light;\n /*\n * Paint the closed box with a layer that follows the surrounding text colour, so it matches\n * whichever theme is active instead of hardcoding a dark value into a light theme.\n */\n background-color: color-mix(in srgb, currentColor 8%, transparent);\n color: inherit;\n cursor: pointer;\n padding-right: 22px;\n /* Room for the chevron drawn below, so text never runs under it. */\n appearance: none;\n background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),\n linear-gradient(135deg, currentColor 50%, transparent 50%);\n background-position: calc(100% - 12px) calc(50% + 1px), calc(100% - 8px) calc(50% + 1px);\n background-size: 4px 4px, 4px 4px;\n background-repeat: no-repeat;\n}\n\nselect.ad-input:hover:not(:disabled) {\n background-color: color-mix(in srgb, currentColor 14%, transparent);\n}\n\nselect.ad-input:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 1px;\n}\n\n/*\n * The popup itself follows `color-scheme`; these keep the option rows explicit for engines that\n * honour per-option colours. `color-scheme: dark` above is what actually stops the light-on-dark\n * list, because the popup is native chrome rather than a styled element.\n */\nselect.ad-input option {\n background-color: Canvas;\n color: CanvasText;\n}\n\n.ad-fieldError {\n margin: 0;\n font-size: 11px;\n color: #c53030;\n}\n\n/* โโ key row โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\n\n/*\n * The key line states a FACT and offers the fix beside it: \"configured / not configured\",\n * the reference name the secret is filed under, and one text button that opens the input.\n *\n * The value is never shown โ the harness credential seam answers \"configured?\" and refuses to\n * answer anything else, so there is nothing to render even if we wanted to.\n *\n * ๐ด `inline-flex`, not `flex`, and NO vertical padding: since the compactness pass this sits\n * INSIDE `.ad-rowDetail` (one wrapping line), so a block-level flex box with `padding-top: 2px`\n * would add a break and a 2px bump to every row. Inline keeps \"ๅฏ้ฅๅทฒ้
็ฝฎ ๆดๆข\" together as one\n * token in the flow.\n */\n.ad-keyLine {\n display: inline-flex;\n flex-wrap: nowrap;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n}\n\n.ad-keyOn {\n color: #2f855a;\n}\n\n.ad-keyOff {\n color: #b7791f;\n}\n\n/*\n * Remaining balance.\n *\n * Same line shape as the key state โ it is another fact about this channel, not a control. The\n * subtle fill marks it as a measurement (like `ad-keyName`) rather than a warning: the words\n * themselves say whether it is fine (\"72.50 USD\") or not (\"๏ผไฝ้ขไธ่ถณ๏ผ\").\n */\n.ad-balance {\n font-variant-numeric: tabular-nums;\n font-size: 11px;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--ad-fill-subtle);\n}\n\n/* A section heading with one action on its right (currently: ๆฅ่ฏขไฝ้ข). */\n.ad-sectionBar {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n}\n\n/* The reference name, not a secret: monospace so it lines up and misreads as a name, not a value. */\n.ad-keyName {\n font-size: 10px;\n padding: 1px 5px;\n border-radius: 4px;\n background: var(--ad-fill-subtle);\n opacity: 0.8;\n}\n\n/* A text-shaped action. Styled as a link because it opens an inline editor, not a dialog. */\n.ad-link {\n font-size: 11px;\n padding: 0;\n border: none;\n background: none;\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n opacity: 0.75;\n cursor: pointer;\n}\n\n.ad-link:hover {\n opacity: 1;\n}\n\n/* The inline key editor: one masked field and one button, on the same line as the fact above. */\n.ad-keyForm {\n display: flex;\n align-items: center;\n gap: 6px;\n padding-top: 4px;\n}\n\n.ad-keyForm .ad-input {\n flex: 1;\n min-width: 0;\n}\n\n/* A destructive action is quiet until hovered: it should be findable, not inviting. */\n.ad-btn-danger {\n opacity: 0.6;\n}\n\n.ad-btn-danger:hover:not(:disabled) {\n opacity: 1;\n border-color: var(--ad-danger);\n color: var(--ad-danger);\n}\n\n/*\n * The one hint that must not be skimmed past: where the key ends up.\n *\n * It carries a left rule and slightly higher opacity than an ordinary hint, because a reader who\n * skips it has been told something false by omission โ the key IS in the profile document.\n */\n.ad-hint-warn {\n padding: 8px 10px;\n border-left: 3px solid var(--ad-line-strong);\n background: var(--ad-fill-subtle);\n border-radius: 0 6px 6px 0;\n opacity: 0.82;\n}\n\n/*\n * The table can be wider than a narrow settings pane (ten columns of numbers). Scrolling it\n * inside its own box keeps the tab strip and the channel list reachable, instead of pushing the\n * whole panel sideways.\n */\n.ad-tableWrap {\n overflow-x: auto;\n /* A hint that there is more to the right, without a scrollbar competing with the borders. */\n scrollbar-width: thin;\n}\n\n.ad-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 11px;\n}\n\n/* The first column is a name, not a number: keep it from being squeezed by the numeric columns. */\n.ad-table th:first-child,\n.ad-table td:first-child {\n white-space: nowrap;\n padding-right: 14px;\n}\n\n/* โโ footer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\n\n/*\n * States what this thing is (MIT, open, improvable) and where it lives. A user deciding whether\n * to fork it or add a relay should not have to search for the source.\n */\n.ad-foot {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 8px;\n padding-top: 10px;\n border-top: 1px solid var(--ad-line);\n font-size: 11px;\n opacity: 0.6;\n}\n\n.ad-footLink {\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 10px;\n}\n\n.ad-footLink:hover {\n opacity: 0.9;\n}\n";
|
|
192
205
|
if (typeof document !== "undefined" && document.querySelector("style[data-ad-css=\"SettingsPage\"]") === null) {
|
|
193
206
|
const tag = document.createElement("style");
|
|
194
207
|
tag.setAttribute("data-ad-css", "SettingsPage");
|
|
@@ -649,67 +662,75 @@ window.__ModuleLoader__.load({
|
|
|
649
662
|
})]
|
|
650
663
|
}),
|
|
651
664
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
652
|
-
className: "ad-
|
|
665
|
+
className: "ad-rowDetail",
|
|
653
666
|
children: [
|
|
654
667
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
655
668
|
className: "ad-mono",
|
|
669
|
+
title: "ไธๆธธๆจกๅ id",
|
|
656
670
|
children: row.model
|
|
657
671
|
}),
|
|
658
672
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
659
673
|
className: "ad-sep",
|
|
660
674
|
children: "ยท"
|
|
661
675
|
}),
|
|
662
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
676
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
677
|
+
title: "ๆฅๅฃๅ่ฎฎ",
|
|
678
|
+
children: row.api
|
|
679
|
+
}),
|
|
663
680
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
664
681
|
className: "ad-sep",
|
|
665
682
|
children: "ยท"
|
|
666
683
|
}),
|
|
667
684
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
668
685
|
className: "ad-mono",
|
|
686
|
+
title: "็ซฏ็นๅฐๅ",
|
|
669
687
|
children: row.baseURL
|
|
670
|
-
})
|
|
671
|
-
]
|
|
672
|
-
}),
|
|
673
|
-
probeResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
674
|
-
className: `ad-probe ad-probe-${probeResult.ok ? "ok" : "bad"}`,
|
|
675
|
-
children: probeResult.ok ? `้๏ผ${probeResult.ms}ms๏ผ` : `ไธ้๏ผ${probeResult.code ?? "ERROR"}${probeResult.message === void 0 ? "" : ` โ ${probeResult.message}`}`
|
|
676
|
-
}),
|
|
677
|
-
row.keyConfigured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
678
|
-
className: "ad-keyLine",
|
|
679
|
-
children: [
|
|
680
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
681
|
-
className: "ad-keyOn",
|
|
682
|
-
children: "ๅฏ้ฅๅทฒ้
็ฝฎ"
|
|
683
688
|
}),
|
|
684
|
-
|
|
685
|
-
className:
|
|
686
|
-
children:
|
|
689
|
+
probeResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
690
|
+
className: `ad-probe ad-probe-${probeResult.ok ? "ok" : "bad"}`,
|
|
691
|
+
children: probeResult.ok ? `้๏ผ${probeResult.ms}ms๏ผ` : `ไธ้๏ผ${probeResult.code ?? "ERROR"}${probeResult.message === void 0 ? "" : ` โ ${probeResult.message}`}`
|
|
687
692
|
}),
|
|
688
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
+
row.keyConfigured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
694
|
+
className: "ad-keyLine",
|
|
695
|
+
children: [
|
|
696
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
697
|
+
className: "ad-keyOn",
|
|
698
|
+
children: "ๅฏ้ฅๅทฒ้
็ฝฎ"
|
|
699
|
+
}),
|
|
700
|
+
row.keyKind === "reference" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
701
|
+
className: "ad-mono ad-keyName",
|
|
702
|
+
children: row.keyRef
|
|
703
|
+
}),
|
|
704
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
705
|
+
type: "button",
|
|
706
|
+
className: "ad-link",
|
|
707
|
+
onClick: (event) => {
|
|
708
|
+
event.preventDefault();
|
|
709
|
+
setKeying(keying === row.id ? null : row.id);
|
|
710
|
+
},
|
|
711
|
+
children: keying === row.id ? "ๅๆถ" : "ๆดๆข"
|
|
712
|
+
})
|
|
713
|
+
]
|
|
714
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
715
|
+
className: "ad-keyLine",
|
|
716
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
717
|
+
className: "ad-keyOff",
|
|
718
|
+
children: "ๆช้
็ฝฎๅฏ้ฅ"
|
|
719
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
720
|
+
type: "button",
|
|
721
|
+
className: "ad-link",
|
|
722
|
+
onClick: (event) => {
|
|
723
|
+
event.preventDefault();
|
|
724
|
+
setKeying(keying === row.id ? null : row.id);
|
|
725
|
+
},
|
|
726
|
+
children: keying === row.id ? "ๅๆถ" : "ๅกซๅๅฏ้ฅ"
|
|
727
|
+
})]
|
|
728
|
+
}),
|
|
729
|
+
balances[row.id] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
730
|
+
className: "ad-balance",
|
|
731
|
+
children: formatBalance(balances[row.id])
|
|
693
732
|
})
|
|
694
733
|
]
|
|
695
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
696
|
-
className: "ad-keyLine",
|
|
697
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
698
|
-
className: "ad-keyOff",
|
|
699
|
-
children: "ๆช้
็ฝฎๅฏ้ฅ"
|
|
700
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
701
|
-
type: "button",
|
|
702
|
-
className: "ad-link",
|
|
703
|
-
onClick: () => setKeying(keying === row.id ? null : row.id),
|
|
704
|
-
children: keying === row.id ? "ๅๆถ" : "ๅกซๅๅฏ้ฅ"
|
|
705
|
-
})]
|
|
706
|
-
}),
|
|
707
|
-
balances[row.id] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
708
|
-
className: "ad-keyLine",
|
|
709
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
710
|
-
className: "ad-balance",
|
|
711
|
-
children: formatBalance(balances[row.id])
|
|
712
|
-
})
|
|
713
734
|
}),
|
|
714
735
|
keying === row.id && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
715
736
|
className: "ad-keyForm",
|
|
@@ -1005,6 +1026,11 @@ window.__ModuleLoader__.load({
|
|
|
1005
1026
|
className: "ad-num",
|
|
1006
1027
|
children: "ๅ่ฎก"
|
|
1007
1028
|
}),
|
|
1029
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
1030
|
+
className: "ad-num",
|
|
1031
|
+
title: "ๆฏๆฌก่ฏทๆฑๅนณๅๆบๅธฆ็ token ๆฐ๏ผๅซ็ผๅญๅฝไธญ๏ผโโ ไธๆจกๅ็ 1M ็ชๅฃๅฏน็
ง็๏ผๅฏๅคๆๆฌ็ฑๅฏน่ฏ้ฟๅบฆ่ฟๆฏ็ชๅฃ้ฉฑๅจ",
|
|
1032
|
+
children: "ๅนณๅไธไธๆ"
|
|
1033
|
+
}),
|
|
1008
1034
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
1009
1035
|
className: "ad-num",
|
|
1010
1036
|
title: "็ๆ้ถๆฎต้ๅบฆ๏ผไธๅซ้ฆๅญ็ญๅพ
๏ผ",
|
|
@@ -1045,6 +1071,10 @@ window.__ModuleLoader__.load({
|
|
|
1045
1071
|
className: "ad-num",
|
|
1046
1072
|
children: formatTokens(row.aggregate.total)
|
|
1047
1073
|
}),
|
|
1074
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1075
|
+
className: "ad-num",
|
|
1076
|
+
children: formatMeanContext(row.meanContext)
|
|
1077
|
+
}),
|
|
1048
1078
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1049
1079
|
className: "ad-num",
|
|
1050
1080
|
children: formatSpeed(row.tokensPerSecond)
|
|
@@ -1085,6 +1115,10 @@ window.__ModuleLoader__.load({
|
|
|
1085
1115
|
className: "ad-num",
|
|
1086
1116
|
children: formatTokens(stats.total.total)
|
|
1087
1117
|
}),
|
|
1118
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1119
|
+
className: "ad-num",
|
|
1120
|
+
children: formatMeanContext(stats.meanContext)
|
|
1121
|
+
}),
|
|
1088
1122
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1089
1123
|
className: "ad-num",
|
|
1090
1124
|
children: formatSpeed(stats.totalSpeed)
|
|
@@ -1104,7 +1138,9 @@ window.__ModuleLoader__.load({
|
|
|
1104
1138
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "่พๅ
ฅ" }),
|
|
1105
1139
|
"ๅช็ฎๆชๅฝไธญ็ผๅญ็่พๅ
ฅ๏ผ็ผๅญ่ฏป / ็ผๅญๅๅ็ฌๆๅ๏ผ ไธ่
็ธๅ ๆๆฏ",
|
|
1106
1140
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "่ฎก่ดน่พๅ
ฅ" }),
|
|
1107
|
-
"ใ
|
|
1141
|
+
"ใ",
|
|
1142
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "ๅนณๅไธไธๆ" }),
|
|
1143
|
+
"ๆฏๆฏๆฌก่ฏทๆฑๅนณๅๆบๅธฆ็ token ๆฐ๏ผๅซ็ผๅญๅฝไธญ๏ผโโ ไธๆจกๅ็ 1M ็ชๅฃๅฏน็
ง็๏ผ่ฟไฝไบ็ชๅฃ่ฏดๆๆๆฌ็ฑๅฏน่ฏ้ฟๅบฆ้ฉฑๅจ๏ผไธๆฏ็ชๅฃใ ้ๅบฆๅช็ฎ็ๆ้ถๆฎต๏ผๆปๆถ้ฟๅๅป้ฆๅญ็ญๅพ
๏ผ๏ผ",
|
|
1108
1144
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1109
1145
|
className: "ad-mono",
|
|
1110
1146
|
children: "โ"
|
|
@@ -1116,6 +1152,14 @@ window.__ModuleLoader__.load({
|
|
|
1116
1152
|
" ๆฌก่ฏทๆฑไธๆธธๆชๅๆฅ็จ้๏ผไธ่ฎกๅ
ฅๅ่ฎกใ"
|
|
1117
1153
|
] })
|
|
1118
1154
|
]
|
|
1155
|
+
}),
|
|
1156
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1157
|
+
className: "ad-note",
|
|
1158
|
+
children: [
|
|
1159
|
+
"่ๅด๏ผ",
|
|
1160
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "่ฟ 7 ๅคฉ" }),
|
|
1161
|
+
"ใๆดๆฉ็่ดฆๆฌไปๅจ็ฃ็ไธ๏ผๆๆ็ JSONL๏ผๆชๅ ้ค๏ผ๏ผ ๅชๆฏ่ฟไธช่งๅพ้ป่ฎคไธๅฑ็คบโโๅๅฒ่ฎฐๅฝๆฏ่ฑ้็ๅฏไธๅญๆฎ๏ผๅ ๆๅฐฑๅไธๆฅไบใ"
|
|
1162
|
+
]
|
|
1119
1163
|
})
|
|
1120
1164
|
]
|
|
1121
1165
|
}),
|
package/lib/index.js
CHANGED
|
@@ -329,7 +329,44 @@ const CHANNEL_PRESETS = {
|
|
|
329
329
|
model: "deepseek-v4-flash-202605",
|
|
330
330
|
contextWindow: 1024e3,
|
|
331
331
|
maxTokens: 384e3,
|
|
332
|
-
|
|
332
|
+
/**
|
|
333
|
+
* ๐ด **`image` is declared, and the earlier `['text']` was a FABRICATED justification.**
|
|
334
|
+
*
|
|
335
|
+
* 0.0.18 shipped `input: ['text']` with the comment "per the owner's own live profile
|
|
336
|
+
* (`input: [text]`)". That citation was false: the owner's profile declares BOTH. Measured
|
|
337
|
+
* from this machine's own composed profile tree
|
|
338
|
+
* (`msm("dsh-develop", ["dump-config"])` โ `llm-pi-ai.providers.zy-tecenthub.models[0]`):
|
|
339
|
+
*
|
|
340
|
+
* id: deepseek-v4-flash-202605
|
|
341
|
+
* input: [text, image]
|
|
342
|
+
*
|
|
343
|
+
* This is the same class of error as the "Probed live" comment this repo retracted once
|
|
344
|
+
* already: an assertion of evidence nobody gathered. It is worse than silence here because
|
|
345
|
+
* it made a REAL defect look like a documented endpoint limitation โ see the failure
|
|
346
|
+
* mechanism below.
|
|
347
|
+
*
|
|
348
|
+
* ## What the false declaration actually did (owner report 2026-10-23: ใ่ฝ็จ๏ผไฝๆถไธๅฐๅพใ)
|
|
349
|
+
*
|
|
350
|
+
* A channel that omits `image` does not merely "not offer" images โ pi-ai REWRITES them out
|
|
351
|
+
* on the way to the wire: `downgradeUnsupportedImages()` replaces every image block with
|
|
352
|
+
* `"(image omitted: model does not support images)"` when `!model.input.includes('image')`
|
|
353
|
+
* (`pi-ai/dist/api/transform-messages.js:38-57`, applied at
|
|
354
|
+
* `openai-completions.js:899`). The request still succeeds, nothing throws, and the model
|
|
355
|
+
* answers as though the message were text โ the exact silent shape the decision-32 fix was
|
|
356
|
+
* written to remove. The declaration IS the delivery switch.
|
|
357
|
+
*
|
|
358
|
+
* ## Why `image` is the honest value here
|
|
359
|
+
*
|
|
360
|
+
* Two independent records agree, and neither is an inference:
|
|
361
|
+
* ยท the owner's own working profile (quoted above);
|
|
362
|
+
* ยท Tencent's model list, Chinese edition, updated 2026-10-21, lists this exact id with
|
|
363
|
+
* ใๅพ็็่งฃใ: https://cloud.tencent.com/document/product/1823/130051
|
|
364
|
+
* โ ๏ธ The INTL edition of the same page (updated 2026-09-25) omits it. Both are official and
|
|
365
|
+
* they disagree; the newer one is the one to follow, and the divergence is recorded in
|
|
366
|
+
* `note` rather than hidden โ so the next reader knows this claim had a live dispute, not a
|
|
367
|
+
* clean consensus.
|
|
368
|
+
*/
|
|
369
|
+
input: ["text", "image"],
|
|
333
370
|
reasoningEfforts: { ...V41_FLASH_CAPS.reasoningEfforts },
|
|
334
371
|
compat: { ...DEEPSEEK_THINKING_COMPAT },
|
|
335
372
|
/**
|
|
@@ -348,7 +385,7 @@ const CHANNEL_PRESETS = {
|
|
|
348
385
|
* ๆฅ่ฏขๅณๅฏใ). Declaring a speculative `openai-billing` shape would have produced a 404 on
|
|
349
386
|
* every check, which is the mistake this repo already made once and recorded.
|
|
350
387
|
*/
|
|
351
|
-
note: "Tencent Cloud forwarding endpoint; model id is frozen with a -202605 suffix.
|
|
388
|
+
note: "Tencent Cloud forwarding endpoint; model id is frozen with a -202605 suffix. Takes images (owner profile + Tencent model list, 2026-10-21). No balance API โ the panel verifies liveness instead."
|
|
352
389
|
},
|
|
353
390
|
ollama: {
|
|
354
391
|
displayName: "Ollama Cloud",
|
|
@@ -1596,6 +1633,39 @@ function meanFirstTokenMs(aggregate) {
|
|
|
1596
1633
|
if (aggregate.timed <= 0) return null;
|
|
1597
1634
|
return aggregate.ttftMs / aggregate.timed;
|
|
1598
1635
|
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Mean context size per request, in tokens โ the owner's ใๅนณๅ็ไธไธๆ็ชๅฃๅคงๅฐใ.
|
|
1638
|
+
*
|
|
1639
|
+
* ## What this number is, precisely
|
|
1640
|
+
*
|
|
1641
|
+
* It is the mean of *tokens carried by one request*, not of the model's capacity: the model's
|
|
1642
|
+
* window is 1M for every channel, so reporting that number again would say nothing about how the
|
|
1643
|
+
* thing is actually used. The useful reading is the gap between the two โ a mean context far
|
|
1644
|
+
* below the window means the window is not the constraint and cost is driven by conversation
|
|
1645
|
+
* length, which is what makes this number a cost-optimisation reference rather than trivia.
|
|
1646
|
+
*
|
|
1647
|
+
* Numerator is `total` (= billable input + output), i.e. everything that crossed the wire for
|
|
1648
|
+
* that request. Cache hits are INCLUDED: a cached token still occupies the window even though it
|
|
1649
|
+
* is billed at a lower rate, so leaving it out would understate context pressure.
|
|
1650
|
+
*
|
|
1651
|
+
* ## Why the denominator is not `requests`
|
|
1652
|
+
*
|
|
1653
|
+
* `requests` counts every call, including the ones the relay answered without reporting usage
|
|
1654
|
+
* (`unreported`). Those contributed nothing to the numerator, so dividing by them would silently
|
|
1655
|
+
* deflate the average โ the same trap `timed` exists to avoid for the speed figures.
|
|
1656
|
+
*
|
|
1657
|
+
* ๐ด **Not `requests - unreported - failures` either.** A failed call that reported no usage is
|
|
1658
|
+
* already inside `unreported` (`addToAggregate` increments both), so subtracting `failures` as
|
|
1659
|
+
* well would remove it twice and overstate the mean. The single correct denominator is the count
|
|
1660
|
+
* of entries that actually contributed to `total`, which is exactly `requests - unreported`.
|
|
1661
|
+
* @param aggregate - one group's figures.
|
|
1662
|
+
* @returns mean tokens per usage-reporting request, or null when none reported.
|
|
1663
|
+
*/
|
|
1664
|
+
function meanContextTokens(aggregate) {
|
|
1665
|
+
const reported = aggregate.requests - aggregate.unreported;
|
|
1666
|
+
if (reported <= 0) return null;
|
|
1667
|
+
return aggregate.total / reported;
|
|
1668
|
+
}
|
|
1599
1669
|
/** Empty aggregate, used both as an accumulator seed and as a zero row. */
|
|
1600
1670
|
function emptyAggregate() {
|
|
1601
1671
|
return {
|
|
@@ -2482,8 +2552,9 @@ function registerStatsRoutes(ctx, webServer, deps) {
|
|
|
2482
2552
|
path: `${ROUTE_PREFIX}/stats`,
|
|
2483
2553
|
handler: async (req, res) => {
|
|
2484
2554
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2485
|
-
const
|
|
2486
|
-
const to = queryDate(url.searchParams.get("to"));
|
|
2555
|
+
const requestedFrom = queryDate(url.searchParams.get("from"));
|
|
2556
|
+
const to = queryDate(url.searchParams.get("to")) ?? (requestedFrom === void 0 ? /* @__PURE__ */ new Date() : void 0);
|
|
2557
|
+
const from = requestedFrom ?? (to === void 0 ? void 0 : /* @__PURE__ */ new Date(to.getTime() - 6048e5));
|
|
2487
2558
|
const group = url.searchParams.get("group") ?? "channel";
|
|
2488
2559
|
const entries = await readLedgerRange(deps.ledgerDir(), from, to);
|
|
2489
2560
|
const rows = group === "day" ? summarize(entries, (entry) => dayKey(new Date(entry.at))) : summarize(entries, (entry) => entry.channel);
|
|
@@ -2506,7 +2577,8 @@ function registerStatsRoutes(ctx, webServer, deps) {
|
|
|
2506
2577
|
...row,
|
|
2507
2578
|
label: group === "day" ? row.key : labels.get(row.key) ?? row.key,
|
|
2508
2579
|
tokensPerSecond: outputTokensPerSecond(row.aggregate),
|
|
2509
|
-
firstTokenMs: meanFirstTokenMs(row.aggregate)
|
|
2580
|
+
firstTokenMs: meanFirstTokenMs(row.aggregate),
|
|
2581
|
+
meanContext: meanContextTokens(row.aggregate)
|
|
2510
2582
|
}));
|
|
2511
2583
|
const totalAggregate = totalOf(rows);
|
|
2512
2584
|
json(res, 200, {
|
|
@@ -2524,7 +2596,8 @@ function registerStatsRoutes(ctx, webServer, deps) {
|
|
|
2524
2596
|
*/
|
|
2525
2597
|
totalSpeed: outputTokensPerSecond(totalAggregate),
|
|
2526
2598
|
totalFirstTokenMs: meanFirstTokenMs(totalAggregate),
|
|
2527
|
-
|
|
2599
|
+
meanContext: meanContextTokens(totalAggregate),
|
|
2600
|
+
note: "input is uncached input only; billable input = input + cacheRead + cacheWrite; tokensPerSecond counts generation time only (total minus time-to-first-token); meanContext is total tokens per usage-reporting request"
|
|
2528
2601
|
});
|
|
2529
2602
|
}
|
|
2530
2603
|
},
|
package/lib/usage-ledger.d.ts
CHANGED
|
@@ -107,6 +107,35 @@ export declare function outputTokensPerSecond(aggregate: LedgerAggregate): numbe
|
|
|
107
107
|
* @param aggregate - one group's figures.
|
|
108
108
|
*/
|
|
109
109
|
export declare function meanFirstTokenMs(aggregate: LedgerAggregate): number | null;
|
|
110
|
+
/**
|
|
111
|
+
* Mean context size per request, in tokens โ the owner's ใๅนณๅ็ไธไธๆ็ชๅฃๅคงๅฐใ.
|
|
112
|
+
*
|
|
113
|
+
* ## What this number is, precisely
|
|
114
|
+
*
|
|
115
|
+
* It is the mean of *tokens carried by one request*, not of the model's capacity: the model's
|
|
116
|
+
* window is 1M for every channel, so reporting that number again would say nothing about how the
|
|
117
|
+
* thing is actually used. The useful reading is the gap between the two โ a mean context far
|
|
118
|
+
* below the window means the window is not the constraint and cost is driven by conversation
|
|
119
|
+
* length, which is what makes this number a cost-optimisation reference rather than trivia.
|
|
120
|
+
*
|
|
121
|
+
* Numerator is `total` (= billable input + output), i.e. everything that crossed the wire for
|
|
122
|
+
* that request. Cache hits are INCLUDED: a cached token still occupies the window even though it
|
|
123
|
+
* is billed at a lower rate, so leaving it out would understate context pressure.
|
|
124
|
+
*
|
|
125
|
+
* ## Why the denominator is not `requests`
|
|
126
|
+
*
|
|
127
|
+
* `requests` counts every call, including the ones the relay answered without reporting usage
|
|
128
|
+
* (`unreported`). Those contributed nothing to the numerator, so dividing by them would silently
|
|
129
|
+
* deflate the average โ the same trap `timed` exists to avoid for the speed figures.
|
|
130
|
+
*
|
|
131
|
+
* ๐ด **Not `requests - unreported - failures` either.** A failed call that reported no usage is
|
|
132
|
+
* already inside `unreported` (`addToAggregate` increments both), so subtracting `failures` as
|
|
133
|
+
* well would remove it twice and overstate the mean. The single correct denominator is the count
|
|
134
|
+
* of entries that actually contributed to `total`, which is exactly `requests - unreported`.
|
|
135
|
+
* @param aggregate - one group's figures.
|
|
136
|
+
* @returns mean tokens per usage-reporting request, or null when none reported.
|
|
137
|
+
*/
|
|
138
|
+
export declare function meanContextTokens(aggregate: LedgerAggregate): number | null;
|
|
110
139
|
/** Aggregation result for one grouping. */
|
|
111
140
|
export interface LedgerSummary {
|
|
112
141
|
/** Group key (channel id, or `YYYY-MM-DD`). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-any-deepseek",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "One DeepSeek V4.1 Flash provider for DeepSeek Harness: paste a key per channel, switch channels manually, token usage booked per real upstream.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|