dsh-any-deepseek 0.0.18 โ 0.0.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.
- package/lib/client.js +80 -89
- 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 /*\n * ๐ด The ACTIVE channel is green (owner request 2026-10-25: ใๆฟๆดป็ๆ ทๅผ็จ็ปฟ่ฒใ).\n *\n * It used to be derived from `currentColor`, which made the live row look like every other\n * \"slightly emphasised\" row โ the same neutral tint as a hover or a selected input. Green says\n * \"this one is ON\" in a way a grey tint cannot, and it matches the key-configured dot and the\n * ๅฏ้ฅๅทฒ้
็ฝฎ text that were already green, so the panel now uses one colour for one meaning:\n * green = working / live. Red stays reserved for destructive and for failures.\n *\n * Kept as one token so the mark (border + bar + fill + badge) can never drift apart between\n * the four places it is applied.\n */\n --ad-active: #2f855a;\n --ad-active-fill: color-mix(in srgb, var(--ad-active) 12%, 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 in GREEN (owner 2026-10-25: ใๆฟๆดป็ๆ ทๅผ็จ็ปฟ่ฒใ).\n *\n * Previously the marks were derived from `currentColor` โ a neutral bar, a stronger border and a\n * grey tint. All three were so low-contrast that the live row read as \"merely hovered\", which is\n * exactly what the owner was reacting to.\n *\n * Green is also the coherent choice: the key dot and ๅฏ้ฅๅทฒ้
็ฝฎ text were already green, so the\n * panel now uses ONE colour for ONE meaning (green = working / live), and red stays reserved for\n * destructive actions and failures.\n *\n * Four marks read from the same token so they cannot drift apart: left bar, border, fill, badge.\n */\n.ad-row-current {\n border-color: var(--ad-active);\n border-left-color: var(--ad-active);\n background: var(--ad-active-fill);\n}\n\n/* The ๅฝๅ badge is the textual confirmation, for when colour alone is not enough (colour-blind\n readers, screenshots, forced-colours mode). */\n.ad-row-current .ad-badge {\n border-color: var(--ad-active);\n color: var(--ad-active);\n opacity: 1;\n font-weight: 600;\n}\n\n/* The live row's NAME goes green too: the name is where the eye lands first, so marking only the\n border leaves the reader to connect two separate cues. */\n.ad-row-current .ad-rowTitle {\n color: var(--ad-active);\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 live-state line under a channel's name: probe verdict and/or balance, nothing else.\n *\n * ๐ด It shrank twice at the owner's request, and the history matters:\n * 2026-10-24 ใ็ดงๅไบใ โ four stacked lines (endpoint identity / key state / verdict / balance)\n * became one wrapping line;\n * 2026-10-25 ใ่ฟ้จๅไธ่ฆไบใ โ endpoint identity and key state were removed outright. What\n * remains is only what changes at runtime: did the probe pass, what is\n * the balance. Those two cannot be derived from anything else on screen,\n * which is why they survived while model/api/baseURL did not.\n *\n * The element is rendered ONLY when one of them exists, so an unprobed, unpriced channel shows a\n * single-line row with no orphan gap.\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/* โโ balance โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */\n\n/*\n * Remaining balance.\n *\n * A fact about this channel, not a control. The subtle fill marks it as a measurement rather than\n * a warning: the words 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");
|
|
@@ -609,18 +622,6 @@ window.__ModuleLoader__.load({
|
|
|
609
622
|
className: "ad-warn",
|
|
610
623
|
title: row.warnings.join("\n"),
|
|
611
624
|
children: "โ "
|
|
612
|
-
}),
|
|
613
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
614
|
-
type: "button",
|
|
615
|
-
className: "ad-link ad-renameBtn",
|
|
616
|
-
title: "ๆนๆพ็คบๅ๏ผๆธ ้ id ไธๅ๏ผ",
|
|
617
|
-
onClick: (event) => {
|
|
618
|
-
event.preventDefault();
|
|
619
|
-
event.stopPropagation();
|
|
620
|
-
setRenaming(renaming === row.id ? null : row.id);
|
|
621
|
-
setRenameDraft(renaming === row.id ? "" : row.label);
|
|
622
|
-
},
|
|
623
|
-
children: renaming === row.id ? "ๅๆถ" : "ๆนๅ"
|
|
624
625
|
})
|
|
625
626
|
]
|
|
626
627
|
}),
|
|
@@ -648,68 +649,15 @@ window.__ModuleLoader__.load({
|
|
|
648
649
|
children: "ไฟๅญๅๅญ"
|
|
649
650
|
})]
|
|
650
651
|
}),
|
|
651
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
652
|
-
className: "ad-
|
|
653
|
-
children: [
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}),
|
|
658
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
659
|
-
className: "ad-sep",
|
|
660
|
-
children: "ยท"
|
|
661
|
-
}),
|
|
662
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: row.api }),
|
|
663
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
664
|
-
className: "ad-sep",
|
|
665
|
-
children: "ยท"
|
|
666
|
-
}),
|
|
667
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
668
|
-
className: "ad-mono",
|
|
669
|
-
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
|
-
}),
|
|
684
|
-
row.keyKind === "reference" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
685
|
-
className: "ad-mono ad-keyName",
|
|
686
|
-
children: row.keyRef
|
|
687
|
-
}),
|
|
688
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
689
|
-
type: "button",
|
|
690
|
-
className: "ad-link",
|
|
691
|
-
onClick: () => setKeying(keying === row.id ? null : row.id),
|
|
692
|
-
children: keying === row.id ? "ๅๆถ" : "ๆดๆข"
|
|
693
|
-
})
|
|
694
|
-
]
|
|
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", {
|
|
652
|
+
(probeResult !== void 0 || balances[row.id] !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
653
|
+
className: "ad-rowDetail",
|
|
654
|
+
children: [probeResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
655
|
+
className: `ad-probe ad-probe-${probeResult.ok ? "ok" : "bad"}`,
|
|
656
|
+
children: probeResult.ok ? `้๏ผ${probeResult.ms}ms๏ผ` : `ไธ้๏ผ${probeResult.code ?? "ERROR"}${probeResult.message === void 0 ? "" : ` โ ${probeResult.message}`}`
|
|
657
|
+
}), balances[row.id] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
710
658
|
className: "ad-balance",
|
|
711
659
|
children: formatBalance(balances[row.id])
|
|
712
|
-
})
|
|
660
|
+
})]
|
|
713
661
|
}),
|
|
714
662
|
keying === row.id && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
715
663
|
className: "ad-keyForm",
|
|
@@ -734,21 +682,41 @@ window.__ModuleLoader__.load({
|
|
|
734
682
|
})]
|
|
735
683
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
736
684
|
className: "ad-rowActions",
|
|
737
|
-
children: [
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
685
|
+
children: [
|
|
686
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
687
|
+
type: "button",
|
|
688
|
+
className: "ad-btn ad-btn-quiet",
|
|
689
|
+
title: "ๆนๆพ็คบๅ๏ผๆธ ้ id ไธๅ๏ผ",
|
|
690
|
+
onClick: () => {
|
|
691
|
+
setRenaming(renaming === row.id ? null : row.id);
|
|
692
|
+
setRenameDraft(renaming === row.id ? "" : row.label);
|
|
693
|
+
},
|
|
694
|
+
children: renaming === row.id ? "ๅๆถ" : "ๆนๅ"
|
|
695
|
+
}),
|
|
696
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
697
|
+
type: "button",
|
|
698
|
+
className: "ad-btn ad-btn-quiet",
|
|
699
|
+
title: row.keyConfigured ? "ๆดๆข่ฟไธชๆธ ้็ๅฏ้ฅ" : "ไธบ่ฟไธชๆธ ้ๅกซๅๅฏ้ฅ",
|
|
700
|
+
onClick: () => setKeying(keying === row.id ? null : row.id),
|
|
701
|
+
children: keying === row.id ? "ๅๆถ" : "ๆดๆขๅฏ้ฅ"
|
|
702
|
+
}),
|
|
703
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
704
|
+
type: "button",
|
|
705
|
+
className: "ad-btn ad-btn-quiet",
|
|
706
|
+
disabled: probing === row.id,
|
|
707
|
+
onClick: () => {
|
|
708
|
+
runProbe(row.id);
|
|
709
|
+
},
|
|
710
|
+
children: probing === row.id ? "ๆต่ฏไธญโฆ" : "ๆต่ฏ"
|
|
711
|
+
}),
|
|
712
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
713
|
+
type: "button",
|
|
714
|
+
className: "ad-btn ad-btn-danger",
|
|
715
|
+
title: "็งป้คๆญคๆธ ้๏ผไธๅ ้คๅญๆฎๆฌ่บซ๏ผ",
|
|
716
|
+
onClick: () => removeChannel(row.id),
|
|
717
|
+
children: "็งป้ค"
|
|
718
|
+
})
|
|
719
|
+
]
|
|
752
720
|
})]
|
|
753
721
|
}, row.id);
|
|
754
722
|
})
|
|
@@ -1005,6 +973,11 @@ window.__ModuleLoader__.load({
|
|
|
1005
973
|
className: "ad-num",
|
|
1006
974
|
children: "ๅ่ฎก"
|
|
1007
975
|
}),
|
|
976
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
977
|
+
className: "ad-num",
|
|
978
|
+
title: "ๆฏๆฌก่ฏทๆฑๅนณๅๆบๅธฆ็ token ๆฐ๏ผๅซ็ผๅญๅฝไธญ๏ผโโ ไธๆจกๅ็ 1M ็ชๅฃๅฏน็
ง็๏ผๅฏๅคๆๆฌ็ฑๅฏน่ฏ้ฟๅบฆ่ฟๆฏ็ชๅฃ้ฉฑๅจ",
|
|
979
|
+
children: "ๅนณๅไธไธๆ"
|
|
980
|
+
}),
|
|
1008
981
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
1009
982
|
className: "ad-num",
|
|
1010
983
|
title: "็ๆ้ถๆฎต้ๅบฆ๏ผไธๅซ้ฆๅญ็ญๅพ
๏ผ",
|
|
@@ -1045,6 +1018,10 @@ window.__ModuleLoader__.load({
|
|
|
1045
1018
|
className: "ad-num",
|
|
1046
1019
|
children: formatTokens(row.aggregate.total)
|
|
1047
1020
|
}),
|
|
1021
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1022
|
+
className: "ad-num",
|
|
1023
|
+
children: formatMeanContext(row.meanContext)
|
|
1024
|
+
}),
|
|
1048
1025
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1049
1026
|
className: "ad-num",
|
|
1050
1027
|
children: formatSpeed(row.tokensPerSecond)
|
|
@@ -1085,6 +1062,10 @@ window.__ModuleLoader__.load({
|
|
|
1085
1062
|
className: "ad-num",
|
|
1086
1063
|
children: formatTokens(stats.total.total)
|
|
1087
1064
|
}),
|
|
1065
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1066
|
+
className: "ad-num",
|
|
1067
|
+
children: formatMeanContext(stats.meanContext)
|
|
1068
|
+
}),
|
|
1088
1069
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1089
1070
|
className: "ad-num",
|
|
1090
1071
|
children: formatSpeed(stats.totalSpeed)
|
|
@@ -1104,7 +1085,9 @@ window.__ModuleLoader__.load({
|
|
|
1104
1085
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "่พๅ
ฅ" }),
|
|
1105
1086
|
"ๅช็ฎๆชๅฝไธญ็ผๅญ็่พๅ
ฅ๏ผ็ผๅญ่ฏป / ็ผๅญๅๅ็ฌๆๅ๏ผ ไธ่
็ธๅ ๆๆฏ",
|
|
1106
1087
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "่ฎก่ดน่พๅ
ฅ" }),
|
|
1107
|
-
"ใ
|
|
1088
|
+
"ใ",
|
|
1089
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "ๅนณๅไธไธๆ" }),
|
|
1090
|
+
"ๆฏๆฏๆฌก่ฏทๆฑๅนณๅๆบๅธฆ็ token ๆฐ๏ผๅซ็ผๅญๅฝไธญ๏ผโโ ไธๆจกๅ็ 1M ็ชๅฃๅฏน็
ง็๏ผ่ฟไฝไบ็ชๅฃ่ฏดๆๆๆฌ็ฑๅฏน่ฏ้ฟๅบฆ้ฉฑๅจ๏ผไธๆฏ็ชๅฃใ ้ๅบฆๅช็ฎ็ๆ้ถๆฎต๏ผๆปๆถ้ฟๅๅป้ฆๅญ็ญๅพ
๏ผ๏ผ",
|
|
1108
1091
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1109
1092
|
className: "ad-mono",
|
|
1110
1093
|
children: "โ"
|
|
@@ -1116,6 +1099,14 @@ window.__ModuleLoader__.load({
|
|
|
1116
1099
|
" ๆฌก่ฏทๆฑไธๆธธๆชๅๆฅ็จ้๏ผไธ่ฎกๅ
ฅๅ่ฎกใ"
|
|
1117
1100
|
] })
|
|
1118
1101
|
]
|
|
1102
|
+
}),
|
|
1103
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1104
|
+
className: "ad-note",
|
|
1105
|
+
children: [
|
|
1106
|
+
"่ๅด๏ผ",
|
|
1107
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "่ฟ 7 ๅคฉ" }),
|
|
1108
|
+
"ใๆดๆฉ็่ดฆๆฌไปๅจ็ฃ็ไธ๏ผๆๆ็ JSONL๏ผๆชๅ ้ค๏ผ๏ผ ๅชๆฏ่ฟไธช่งๅพ้ป่ฎคไธๅฑ็คบโโๅๅฒ่ฎฐๅฝๆฏ่ฑ้็ๅฏไธๅญๆฎ๏ผๅ ๆๅฐฑๅไธๆฅไบใ"
|
|
1109
|
+
]
|
|
1119
1110
|
})
|
|
1120
1111
|
]
|
|
1121
1112
|
}),
|
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.20",
|
|
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": {
|