dsh-cline-pass 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/adapter.js +98 -1
- package/lib/client.js +116 -17
- package/lib/index.js +12 -1
- package/lib/panel.js +8 -2
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -68,7 +68,7 @@ To override defaults, add a `cline-pass` section to your profile's `$DSH_HOME/pr
|
|
|
68
68
|
apiKeyEnv: CLINE_PASS_BACKUP_KEY
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Common settings include `baseURL`, `knownModels`, `models`, `perModel`, `exposeCatalog`, and `historyLimit`. The default gateway is `https://api.cline.bot/api/v1`.
|
|
71
|
+
Common settings include `baseURL`, `knownModels`, `models`, `perModel`, `exposeCatalog`, and `historyLimit`. The default gateway is `https://api.cline.bot/api/v1`. `maxConcurrentRequests` caps how many streams each account keeps open to the gateway at once (default 16); further requests wait for a free slot, and `0` lifts the cap.
|
|
72
72
|
|
|
73
73
|
## Upstream channels
|
|
74
74
|
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ cline_pass_accounts action=mode mode=roundrobin
|
|
|
68
68
|
apiKeyEnv: CLINE_PASS_BACKUP_KEY
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
常用设置包括 `baseURL`、`knownModels`、`models`、`perModel`、`exposeCatalog` 和 `historyLimit`。默认网关地址为 `https://api.cline.bot/api/v1
|
|
71
|
+
常用设置包括 `baseURL`、`knownModels`、`models`、`perModel`、`exposeCatalog` 和 `historyLimit`。默认网关地址为 `https://api.cline.bot/api/v1`。`maxConcurrentRequests` 限制每个账号同时发往网关的流数,默认 16,超出的请求排队等待空位,设为 `0` 表示不限。
|
|
72
72
|
|
|
73
73
|
## 上游渠道
|
|
74
74
|
|
package/lib/adapter.js
CHANGED
|
@@ -38,6 +38,15 @@ import { parseServerSentEvents } from './engine.js'
|
|
|
38
38
|
|
|
39
39
|
/** Default maximum idle interval while a stream read is outstanding. */
|
|
40
40
|
export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000
|
|
41
|
+
/**
|
|
42
|
+
* Default number of streams one account keeps open to the gateway at once.
|
|
43
|
+
*
|
|
44
|
+
* Twice dsh 0.1.7's default `maxActiveSubagents` (8): a main agent with a full
|
|
45
|
+
* set of subagents, plus a title or compaction call, never queues, while a
|
|
46
|
+
* runaway fan-out still cannot open an unbounded number of upstream streams
|
|
47
|
+
* (CWE-770). `0` lifts the cap.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_MAX_CONCURRENT_REQUESTS = 16
|
|
41
50
|
/**
|
|
42
51
|
* Conservative context capacity for a model the catalog does not describe.
|
|
43
52
|
* Shipped models carry their published window, so this only bounds the
|
|
@@ -558,6 +567,81 @@ async function* tapRouting(payloads, observed) {
|
|
|
558
567
|
}
|
|
559
568
|
}
|
|
560
569
|
|
|
570
|
+
//#region upstream concurrency
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Per-account slots for streams open to the gateway.
|
|
574
|
+
*
|
|
575
|
+
* A slot is held for a whole attempt — from before `fetch` until the response
|
|
576
|
+
* body is closed — because the stream, not the wait for headers, is what keeps
|
|
577
|
+
* a socket and the account's concurrency busy. Accounts are counted apart, so a
|
|
578
|
+
* stalled account cannot hold up requests routed to a healthy one; absorbing
|
|
579
|
+
* exactly that is what the pool is for.
|
|
580
|
+
*
|
|
581
|
+
* Waiters are served first in, first out, and a freed slot is handed to the
|
|
582
|
+
* next waiter and counted in the same step, so a request arriving in between
|
|
583
|
+
* cannot take it as well. A waiter whose signal aborts leaves the queue at
|
|
584
|
+
* once; a held slot is also freed when its signal aborts, so a consumer that
|
|
585
|
+
* stops reading without closing the stream does not keep it.
|
|
586
|
+
*
|
|
587
|
+
* @returns `{ acquire(key, limit, signal) }`, resolving to a release function
|
|
588
|
+
* that is safe to call more than once. A `limit` below 1 means no limit.
|
|
589
|
+
*/
|
|
590
|
+
export function createUpstreamSlots() {
|
|
591
|
+
const pools = new Map()
|
|
592
|
+
const admit = (pool) => {
|
|
593
|
+
while (pool.waiting.length > 0 && pool.active < pool.limit) {
|
|
594
|
+
pool.active += 1
|
|
595
|
+
pool.waiting.shift()()
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const hold = (pool, signal) => {
|
|
599
|
+
let held = true
|
|
600
|
+
const release = () => {
|
|
601
|
+
if (!held) return
|
|
602
|
+
held = false
|
|
603
|
+
signal?.removeEventListener('abort', release)
|
|
604
|
+
pool.active -= 1
|
|
605
|
+
admit(pool)
|
|
606
|
+
}
|
|
607
|
+
signal?.addEventListener('abort', release, { once: true })
|
|
608
|
+
return release
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
async acquire(key, limit, signal) {
|
|
612
|
+
signal?.throwIfAborted()
|
|
613
|
+
let pool = pools.get(key)
|
|
614
|
+
if (pool === undefined) {
|
|
615
|
+
pool = { active: 0, limit: Infinity, waiting: [] }
|
|
616
|
+
pools.set(key, pool)
|
|
617
|
+
}
|
|
618
|
+
// Read on every call, so a changed setting reaches the next request; a
|
|
619
|
+
// raised limit admits the queue before this caller joins it.
|
|
620
|
+
pool.limit = Number.isFinite(limit) && limit >= 1 ? Math.floor(limit) : Infinity
|
|
621
|
+
admit(pool)
|
|
622
|
+
if (pool.waiting.length === 0 && pool.active < pool.limit) {
|
|
623
|
+
pool.active += 1
|
|
624
|
+
return hold(pool, signal)
|
|
625
|
+
}
|
|
626
|
+
return await new Promise((resolve, reject) => {
|
|
627
|
+
const grant = () => {
|
|
628
|
+
signal?.removeEventListener('abort', abandon)
|
|
629
|
+
resolve(hold(pool, signal))
|
|
630
|
+
}
|
|
631
|
+
const abandon = () => {
|
|
632
|
+
const at = pool.waiting.indexOf(grant)
|
|
633
|
+
if (at !== -1) pool.waiting.splice(at, 1)
|
|
634
|
+
reject(signal.reason)
|
|
635
|
+
}
|
|
636
|
+
pool.waiting.push(grant)
|
|
637
|
+
signal?.addEventListener('abort', abandon, { once: true })
|
|
638
|
+
})
|
|
639
|
+
},
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
//#endregion
|
|
644
|
+
|
|
561
645
|
/**
|
|
562
646
|
* The Cline Pass adapter. One instance serves the plugin's provider route(s);
|
|
563
647
|
* the harness model id IS the gateway's wire model id.
|
|
@@ -566,6 +650,7 @@ export class ClinePassAdapter extends LlmAdapter {
|
|
|
566
650
|
constructor(config) {
|
|
567
651
|
super()
|
|
568
652
|
this.config = config
|
|
653
|
+
this.upstreamSlots = createUpstreamSlots()
|
|
569
654
|
}
|
|
570
655
|
|
|
571
656
|
providerInfo(provider) {
|
|
@@ -668,9 +753,18 @@ export class ClinePassAdapter extends LlmAdapter {
|
|
|
668
753
|
const account = await this.config.resolveAccount({ exclude: [...refused] })
|
|
669
754
|
if (account === undefined) break
|
|
670
755
|
const body = injectPrefs(buildRequestBody(options, connection, images), meta, attempt)
|
|
671
|
-
const attemptStarted = Date.now()
|
|
672
756
|
const consumer = new AbortController()
|
|
673
757
|
const upstream = options.signal === undefined ? consumer.signal : AbortSignal.any([options.signal, consumer.signal])
|
|
758
|
+
// The account's slot is taken before the attempt's clock starts: the wait
|
|
759
|
+
// is local, and counting it would teach the channel a latency it never had.
|
|
760
|
+
let release
|
|
761
|
+
try {
|
|
762
|
+
release = await this.upstreamSlots.acquire(account.name, connection.maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS, upstream)
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (options.signal?.aborted) throw new LlmError('Cline Pass request aborted by caller', 'ABORTED', { cause: error })
|
|
765
|
+
throw error
|
|
766
|
+
}
|
|
767
|
+
const attemptStarted = Date.now()
|
|
674
768
|
const watchdog = idleWatchdog(upstream, connection.streamIdleTimeoutMs, STREAM_IDLE_TIMEOUT_CODE)
|
|
675
769
|
try {
|
|
676
770
|
let response
|
|
@@ -847,6 +941,9 @@ export class ClinePassAdapter extends LlmAdapter {
|
|
|
847
941
|
if (attempt.upstream !== null && adherence === 'adopted') this.config.learnUpstream(options.model, attempt.upstream, 'ok', '', Date.now() - attemptStarted)
|
|
848
942
|
return
|
|
849
943
|
} finally {
|
|
944
|
+
// Held until the attempt ends, response body included, so the slot
|
|
945
|
+
// bounds open streams rather than requests waiting for headers.
|
|
946
|
+
release()
|
|
850
947
|
watchdog[Symbol.dispose]()
|
|
851
948
|
}
|
|
852
949
|
}
|
package/lib/client.js
CHANGED
|
@@ -119,16 +119,43 @@ window.__ModuleLoader__.load({
|
|
|
119
119
|
// track is resolved from that row's own content, so a row whose upstream
|
|
120
120
|
// is a long pill and a row whose upstream is "(auto)" end up with
|
|
121
121
|
// different column widths — and nothing lines up with the header.
|
|
122
|
-
|
|
122
|
+
//
|
|
123
|
+
// `table-layout:auto`, i.e. the browser's own algorithm, and no `min-width`:
|
|
124
|
+
// the table always fits its container, so the log never needs sideways
|
|
125
|
+
// scrolling, and the sizing decision is left to the engine that knows each
|
|
126
|
+
// cell's real content width.
|
|
127
|
+
//
|
|
128
|
+
// That decision matters because the content genuinely does not fit. Measured
|
|
129
|
+
// at this card's real width (about 490px of table inside the settings card,
|
|
130
|
+
// which caps at 760px): the columns need roughly 85 + 208 + 234 + 111 =
|
|
131
|
+
// 638px, so something has to give. Under `table-layout:fixed` plus shares,
|
|
132
|
+
// what gave was the right-hand figures — they were ellipsised to "13…" and
|
|
133
|
+
// "4…", losing the number the reader came for. Left to itself the engine
|
|
134
|
+
// gives way on the token column instead, which simply wraps to a second
|
|
135
|
+
// line: the figures stay whole and nothing is lost.
|
|
136
|
+
'.cp-history-table{width:100%;border-collapse:collapse;table-layout:auto}',
|
|
123
137
|
// Header and data share one alignment: a table whose labels sit left of
|
|
124
138
|
// their values is the thing this panel keeps getting asked to fix.
|
|
125
139
|
'.cp-history-table th{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-1);text-align:center;font-weight:400;font-size:11px;color:var(--dsw-alias-label-tertiary);padding:2px 0 6px;border-bottom:0.5px solid var(--dsw-alias-border-l3)}',
|
|
126
140
|
'.cp-history-table td{padding:6px 0;border-bottom:0.5px solid var(--dsw-alias-border-l2);font-size:12px;vertical-align:middle;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
127
141
|
'.cp-history-table tbody tr:hover td{background:var(--dsw-alias-bg-layer-2)}',
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
// Only two columns are pinned, and each to its measured need:
|
|
143
|
+
// 点 16px — the status dot
|
|
144
|
+
// 时间 82px — a clock, or a dated stamp like "09-24 04:48" (needs 85px,
|
|
145
|
+
// but the engine measures a little differently than the
|
|
146
|
+
// intrinsic probe; 82px is where the stamp stops clipping)
|
|
147
|
+
// TOKEN and 延迟 are left to the engine. 延迟 holds single-line figures, so
|
|
148
|
+
// it always gets what it needs — which is the point: previously the model
|
|
149
|
+
// column took every spare pixel while the figures on the right were clipped,
|
|
150
|
+
// and that is the "left too loose, right too tight" shape being fixed.
|
|
151
|
+
'.cp-history-col-dot{width:16px}',
|
|
152
|
+
'.cp-history-col-when{width:82px}',
|
|
153
|
+
// The columns used to sit flush against one another at `padding:0`, which is
|
|
154
|
+
// what made the right-hand figures read as cramped. A little padding on each
|
|
155
|
+
// side of the three content columns separates label from neighbour.
|
|
156
|
+
'.cp-history-table th:nth-child(2),.cp-history-table td:nth-child(2){padding-left:10px;padding-right:10px}',
|
|
157
|
+
'.cp-history-table th:nth-child(4),.cp-history-table td:nth-child(4){padding-left:10px;padding-right:10px}',
|
|
158
|
+
'.cp-history-table th:nth-child(5),.cp-history-table td:nth-child(5){padding-left:10px;padding-right:10px}',
|
|
132
159
|
'.cp-history-when{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}',
|
|
133
160
|
'.cp-history-model-cell{min-width:0}',
|
|
134
161
|
// The flex stack lives INSIDE the cell, never on the `<td>` itself: a td
|
|
@@ -142,7 +169,20 @@ window.__ModuleLoader__.load({
|
|
|
142
169
|
'.cp-history-table td.cp-history-tokens{color:var(--dsw-alias-label-secondary);font-size:11.5px;font-variant-numeric:tabular-nums;white-space:normal;overflow:visible;text-overflow:clip;line-height:1.5}',
|
|
143
170
|
// Three stacked figures read like a request log: label left, value right.
|
|
144
171
|
'.cp-history-table td.cp-history-load{font-variant-numeric:tabular-nums;white-space:normal;overflow:visible;text-overflow:clip}',
|
|
145
|
-
|
|
172
|
+
// `space-between` is what keeps the three figures right-aligned with each
|
|
173
|
+
// other across rows, but it also turns every spare pixel of the column into
|
|
174
|
+
// a gap between label and value. Capping the row and centring it makes that
|
|
175
|
+
// surplus invisible instead, so the column can be given generous width at a
|
|
176
|
+
// wide panel without the figures drifting apart.
|
|
177
|
+
'.cp-history-load-row{display:flex;justify-content:space-between;gap:5px;max-width:112px;margin:0 auto;font-size:11.5px;line-height:1.5}',
|
|
178
|
+
// With the table no longer allowed to scroll sideways, a narrow pane can
|
|
179
|
+
// leave this column short of its need. The label must not wrap then — a
|
|
180
|
+
// taller cell would change the row height — so the label stays whole and
|
|
181
|
+
// the figure is the part that gives way, ellipsised with its exact value
|
|
182
|
+
// still available in the row tooltip.
|
|
183
|
+
'.cp-history-load-row>span{white-space:nowrap}',
|
|
184
|
+
'.cp-history-load-row>span:first-child{flex:none}',
|
|
185
|
+
'.cp-history-load-row>span:last-child{min-width:0;overflow:hidden;text-overflow:ellipsis;text-align:right}',
|
|
146
186
|
'.cp-history-meta{display:flex;align-items:center;justify-content:center;gap:6px;min-width:0;overflow:hidden}',
|
|
147
187
|
// A failure gets its own row under the entry rather than widening it: the
|
|
148
188
|
// gateway's messages are long enough to wreck a one-line layout.
|
|
@@ -173,8 +213,15 @@ window.__ModuleLoader__.load({
|
|
|
173
213
|
'.cp-fold:hover .cp-chevron{color:var(--dsw-alias-label-primary)}',
|
|
174
214
|
'.cp-set-body{border-top:0.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0 8px}',
|
|
175
215
|
'.cp-usage-groups{display:flex;flex-direction:column;gap:12px}',
|
|
176
|
-
|
|
177
|
-
|
|
216
|
+
// A pool shows one account at a time: the arrows are the only way to reach
|
|
217
|
+
// the others, so they sit on the same row as the name they act on.
|
|
218
|
+
'.cp-usage-pager{display:flex;align-items:center;gap:8px}',
|
|
219
|
+
'.cp-usage-nav{box-sizing:border-box;width:24px;height:24px;flex:none;padding:0;border:0.5px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:14px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}',
|
|
220
|
+
'.cp-usage-nav:hover{border-color:var(--dsw-alias-brand-primary)}',
|
|
221
|
+
'.cp-usage-name{display:flex;align-items:center;gap:8px;min-width:0}',
|
|
222
|
+
'.cp-usage-acct{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
223
|
+
'.cp-usage-active{border:0.5px solid var(--dsw-alias-brand-primary);border-radius:999px;padding:1px 7px;font-size:11px;font-weight:600;flex:none}',
|
|
224
|
+
'.cp-usage-pos{font-size:12px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary)}',
|
|
178
225
|
'.cp-usage{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px}',
|
|
179
226
|
'.cp-usage-item{border:0.5px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}',
|
|
180
227
|
'.cp-usage-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}',
|
|
@@ -262,6 +309,9 @@ window.__ModuleLoader__.load({
|
|
|
262
309
|
usageUnavailable: '暂时读不到官方额度。',
|
|
263
310
|
usageEmpty: '官方未返回额度窗口。',
|
|
264
311
|
usageAccount: '账号 {name}',
|
|
312
|
+
usagePrev: '上一个账号',
|
|
313
|
+
usageNext: '下一个账号',
|
|
314
|
+
usagePosition: '第 {i} / {n} 个',
|
|
265
315
|
historyWhen: '时间',
|
|
266
316
|
historyModel: '模型',
|
|
267
317
|
historyUpstream: '上游',
|
|
@@ -341,6 +391,9 @@ window.__ModuleLoader__.load({
|
|
|
341
391
|
usageUnavailable: 'The official quota is unavailable right now.',
|
|
342
392
|
usageEmpty: 'The gateway reported no quota windows.',
|
|
343
393
|
usageAccount: 'account {name}',
|
|
394
|
+
usagePrev: 'Previous account',
|
|
395
|
+
usageNext: 'Next account',
|
|
396
|
+
usagePosition: '{i} of {n}',
|
|
344
397
|
historyWhen: 'Time',
|
|
345
398
|
historyModel: 'Model',
|
|
346
399
|
historyUpstream: 'Upstream',
|
|
@@ -1443,10 +1496,17 @@ window.__ModuleLoader__.load({
|
|
|
1443
1496
|
* One reading per window: the official percentage, plus when it resets.
|
|
1444
1497
|
*/
|
|
1445
1498
|
function UsageCard(props) {
|
|
1446
|
-
const { t, usage, disabled, actions, unavailable } = props
|
|
1499
|
+
const { t, usage, disabled, actions, unavailable, activeAccount, accountMode } = props
|
|
1447
1500
|
const fetched = Number(usage?.fetchedAt ?? 0) > 0
|
|
1448
1501
|
const accounts = usage?.accounts ?? []
|
|
1449
|
-
|
|
1502
|
+
// A pool is shown one account at a time. Stacking every account turned the
|
|
1503
|
+
// card into a wall of bars and pushed the one reading a user came for below
|
|
1504
|
+
// the fold, so the arrows — not a scroll — are what moves between accounts.
|
|
1505
|
+
const [picked, setPicked] = React.useState(0)
|
|
1506
|
+
// The selection is an index, so a shorter list — an account removed, or its
|
|
1507
|
+
// key deleted — can leave it past the end. Clamp instead of rendering nothing.
|
|
1508
|
+
const index = accounts.length === 0 ? 0 : Math.min(Math.max(picked, 0), accounts.length - 1)
|
|
1509
|
+
const step = (delta) => setPicked((accounts.length + index + delta) % accounts.length)
|
|
1450
1510
|
const windowRow = (type, key, limit) => {
|
|
1451
1511
|
const percent = Math.max(0, Math.min(100, Number(limit.percentUsed ?? 0)))
|
|
1452
1512
|
const left = countdown(t, limit.resetsAt)
|
|
@@ -1464,8 +1524,14 @@ window.__ModuleLoader__.load({
|
|
|
1464
1524
|
left === '' ? null : h('div', { key: 'note', className: 'cp-usage-note' }, fill(t('usageResetsIn'), { t: left })),
|
|
1465
1525
|
])
|
|
1466
1526
|
}
|
|
1467
|
-
/**
|
|
1468
|
-
|
|
1527
|
+
/**
|
|
1528
|
+
* One account's block: a name row that doubles as the account switcher,
|
|
1529
|
+
* then its windows.
|
|
1530
|
+
*
|
|
1531
|
+
* The arrows only exist in a pool. With a single account there is nowhere
|
|
1532
|
+
* to go, so they would be two controls that cannot do anything.
|
|
1533
|
+
*/
|
|
1534
|
+
const accountGroup = (account, at) => {
|
|
1469
1535
|
const limits = account.limits ?? []
|
|
1470
1536
|
const known = new Map(limits.map((limit) => [String(limit.type), limit]))
|
|
1471
1537
|
const windows = [
|
|
@@ -1479,15 +1545,41 @@ window.__ModuleLoader__.load({
|
|
|
1479
1545
|
: windows.length === 0
|
|
1480
1546
|
? h('div', { key: 'none', className: 'cp-muted' }, t('usageEmpty'))
|
|
1481
1547
|
: h('div', { key: 'bars', className: 'cp-usage' }, windows.map(([type, key, limit]) => windowRow(type, key, limit)))
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1548
|
+
const pooled = accounts.length > 1
|
|
1549
|
+
const label = account.displayName || account.account
|
|
1550
|
+
// Only a single-account pool has a "current" account: in round-robin every
|
|
1551
|
+
// request rotates, so a badge here would name a fact that keeps changing.
|
|
1552
|
+
const isActive = accountMode !== 'roundrobin' && activeAccount !== '' && account.account === activeAccount
|
|
1553
|
+
const head = pooled
|
|
1554
|
+
? h('div', { key: 'name', className: 'cp-usage-pager' }, [
|
|
1555
|
+
// Switching accounts re-reads nothing: the whole pool was read in one
|
|
1556
|
+
// request, so paging is local and stays available while the panel is
|
|
1557
|
+
// busy or the settings service is read-only.
|
|
1558
|
+
h('button', {
|
|
1559
|
+
key: 'prev', type: 'button', className: 'cp-usage-nav',
|
|
1560
|
+
title: t('usagePrev'), 'aria-label': t('usagePrev'),
|
|
1561
|
+
onClick: () => step(-1),
|
|
1562
|
+
}, '‹'),
|
|
1563
|
+
h('div', { key: 'who', className: 'cp-usage-name' }, [
|
|
1564
|
+
h('span', { key: 'n', className: 'cp-usage-acct', title: label }, label),
|
|
1565
|
+
isActive ? h('span', { key: 'a', className: 'cp-usage-active' }, t('accountsActive')) : null,
|
|
1566
|
+
h('span', { key: 'p', className: 'cp-usage-pos' }, fill(t('usagePosition'), { i: at + 1, n: accounts.length })),
|
|
1567
|
+
]),
|
|
1568
|
+
h('button', {
|
|
1569
|
+
key: 'next', type: 'button', className: 'cp-usage-nav',
|
|
1570
|
+
title: t('usageNext'), 'aria-label': t('usageNext'),
|
|
1571
|
+
onClick: () => step(1),
|
|
1572
|
+
}, '›'),
|
|
1573
|
+
])
|
|
1574
|
+
: null
|
|
1575
|
+
return h('div', { key: String(account.account), className: 'cp-usage-group' }, [head, inner])
|
|
1486
1576
|
}
|
|
1487
1577
|
let body
|
|
1488
1578
|
if (!fetched) body = h('div', { key: 'wait', className: 'cp-muted' }, unavailable === true ? t('usageUnavailable') : t('usageLoading'))
|
|
1489
1579
|
else if (accounts.length === 0) body = h('div', { key: 'none', className: 'cp-muted' }, t('usageEmpty'))
|
|
1490
|
-
|
|
1580
|
+
// `index` is the account on screen; its own key remounts the block so the
|
|
1581
|
+
// windows never animate between two accounts' readings.
|
|
1582
|
+
else body = h('div', { key: 'groups', className: 'cp-usage-groups' }, accountGroup(accounts[index], index))
|
|
1491
1583
|
const title = accounts.length > 1
|
|
1492
1584
|
? `${t('usageTitle')} · ${fill(t('accountsCount'), { n: accounts.length })}`
|
|
1493
1585
|
: `${t('usageTitle')}${accounts[0] ? ` · ${fill(t('usageAccount'), { name: accounts[0].account })}` : ''}`
|
|
@@ -1607,6 +1699,13 @@ window.__ModuleLoader__.load({
|
|
|
1607
1699
|
key: 'usage',
|
|
1608
1700
|
t,
|
|
1609
1701
|
usage: data.usage,
|
|
1702
|
+
// Which account the pool would use, resolved the same way the accounts
|
|
1703
|
+
// card resolves it: an empty `activeAccount` means "first enabled", not
|
|
1704
|
+
// "none", so the badge has to name that account rather than nobody.
|
|
1705
|
+
activeAccount: data.activeAccount !== ''
|
|
1706
|
+
? data.activeAccount
|
|
1707
|
+
: (data.accounts.find((account) => account.enabled)?.key ?? ''),
|
|
1708
|
+
accountMode: data.accountMode,
|
|
1610
1709
|
unavailable: readFailed,
|
|
1611
1710
|
disabled,
|
|
1612
1711
|
actions,
|
package/lib/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
|
|
|
22
22
|
import {
|
|
23
23
|
ClinePassAdapter,
|
|
24
24
|
DEFAULT_CONTEXT_WINDOW,
|
|
25
|
+
DEFAULT_MAX_CONCURRENT_REQUESTS,
|
|
25
26
|
DEFAULT_MAX_TOKENS,
|
|
26
27
|
DEFAULT_REQUEST_IMAGE_POLICY,
|
|
27
28
|
DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
|
@@ -34,7 +35,7 @@ import { BUILD_TAG } from './protocol.js'
|
|
|
34
35
|
import { createStore } from './store.js'
|
|
35
36
|
import { registerTools } from './tools.js'
|
|
36
37
|
|
|
37
|
-
export { ClinePassAdapter, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_REQUEST_IMAGE_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS }
|
|
38
|
+
export { ClinePassAdapter, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_CONCURRENT_REQUESTS, DEFAULT_MAX_TOKENS, DEFAULT_REQUEST_IMAGE_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS }
|
|
38
39
|
export { collectImageRefs, prepareRequestImages, projectImageDimensions, requestImageTarget } from './adapter.js'
|
|
39
40
|
export { MODEL_CATALOG, REASONING_EFFORTS } from './catalog.js'
|
|
40
41
|
export { createEngine } from './engine.js'
|
|
@@ -136,6 +137,13 @@ export const Config = z.object({
|
|
|
136
137
|
defaultContextWindow: z.number().default(DEFAULT_CONTEXT_WINDOW),
|
|
137
138
|
defaultMaxTokens: z.number().default(DEFAULT_MAX_TOKENS),
|
|
138
139
|
streamIdleTimeoutMs: z.number().default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
140
|
+
/**
|
|
141
|
+
* Streams one account may keep open to the gateway at once.
|
|
142
|
+
*
|
|
143
|
+
* A request past the cap waits for a slot (and can still be cancelled while it
|
|
144
|
+
* waits) instead of opening another stream. `0` lifts the cap.
|
|
145
|
+
*/
|
|
146
|
+
maxConcurrentRequests: z.number().default(DEFAULT_MAX_CONCURRENT_REQUESTS),
|
|
139
147
|
exposeCatalog: z.boolean().default(false),
|
|
140
148
|
historyLimit: z.number().default(100),
|
|
141
149
|
/**
|
|
@@ -303,6 +311,8 @@ export function apply(ctx, config) {
|
|
|
303
311
|
const key = await resolveKey(profile.apiKeyEnv).catch(() => undefined)
|
|
304
312
|
return {
|
|
305
313
|
name,
|
|
314
|
+
// The name a person recognizes, for the panel's account switcher.
|
|
315
|
+
displayName: profile.displayName,
|
|
306
316
|
key: key ?? '',
|
|
307
317
|
keyConfigured: key !== undefined,
|
|
308
318
|
baseURL: profile.baseURL || cfg.baseURL,
|
|
@@ -342,6 +352,7 @@ export function apply(ctx, config) {
|
|
|
342
352
|
maxTokens: cfg.defaultMaxTokens,
|
|
343
353
|
reasoningModels: cfg.reasoningModels,
|
|
344
354
|
streamIdleTimeoutMs: cfg.streamIdleTimeoutMs,
|
|
355
|
+
maxConcurrentRequests: cfg.maxConcurrentRequests,
|
|
345
356
|
}
|
|
346
357
|
}
|
|
347
358
|
|
package/lib/panel.js
CHANGED
|
@@ -155,12 +155,18 @@ export function createPanel({ control, engine, store }) {
|
|
|
155
155
|
const accounts = await control.usageAccounts()
|
|
156
156
|
if (accounts.length === 0) throw new Error('every configured Cline Pass account is disabled; enable one first')
|
|
157
157
|
const readings = await Promise.all(accounts.map(async (account) => {
|
|
158
|
-
|
|
158
|
+
// The reading is keyed by the configuration name, but the panel's switcher
|
|
159
|
+
// shows the name the accounts card shows, so both travel together.
|
|
160
|
+
const label = {
|
|
161
|
+
account: account.name,
|
|
162
|
+
displayName: String(account.displayName ?? '') || String(account.name),
|
|
163
|
+
}
|
|
164
|
+
if (account.keyConfigured !== true) return { ...label, ok: false, limits: [], error: `no API key stored for ${account.name}` }
|
|
159
165
|
const result = await fetchUsageLimits({
|
|
160
166
|
baseURL: account.baseURL || String(config.baseURL ?? ''),
|
|
161
167
|
apiKey: account.key,
|
|
162
168
|
})
|
|
163
|
-
return {
|
|
169
|
+
return { ...label, ...result }
|
|
164
170
|
}))
|
|
165
171
|
const reading = store.setUsage({ accounts: readings })
|
|
166
172
|
return { ...(await state()), usage: reading }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cline-pass",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Self-contained Cline Pass provider for dsh: register Cline Pass subscription models on the harness LLM seam, enumerate and validate the upstream channels behind each model, pin or exclude them with failover, and drive an account pool. No proxy process and no external cline-pass-switcher install required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|