dsh-cline-pass 0.2.6 → 0.2.7
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/adapter.js +15 -4
- package/lib/client.js +27 -14
- package/lib/index.js +2 -2
- package/package.json +2 -2
package/lib/adapter.js
CHANGED
|
@@ -474,9 +474,14 @@ function closeBlock(block) {
|
|
|
474
474
|
/** Map a gateway error payload to a stable harness code. */
|
|
475
475
|
export function streamErrorCode(message, status) {
|
|
476
476
|
const detail = String(message ?? '')
|
|
477
|
-
|
|
477
|
+
// The harness vocabulary covers balances, but Cline also refuses spent
|
|
478
|
+
// subscription windows. Require exhaustion, not just a window name, so a
|
|
479
|
+
// temporary error fetching weekly usage does not stand an account down.
|
|
480
|
+
const windowDetail = detail.replace(/[_-]/g, ' ')
|
|
481
|
+
const spentWindow = /\b(?:weekly|monthly|daily|(?:five|5)\s*hour|plan)\b[\s\S]{0,80}?\b(?:exceed\w*|exhaust\w*|reach\w*|used\s+up|out\s+of|deplet\w*|spent)\b|\b(?:exceed\w*|exhaust\w*|reach\w*|used\s+up|out\s+of|deplet\w*)\b[\s\S]{0,80}?\b(?:weekly|monthly|daily|(?:five|5)\s*hour|plan)\b[\s\S]{0,40}?\b(?:limit|cap|quota|credits|budget|allowance)\b/i.test(windowDetail)
|
|
482
|
+
if (status === 402 || isQuotaExceededError(detail) || spentWindow) return QUOTA_EXCEEDED_CODE
|
|
478
483
|
if (/unauthorized|re-authenticate|invalid api key|401|403/i.test(detail)) return 'AUTH'
|
|
479
|
-
if (/429|rate limit|rate-?limited/i.test(detail)) return 'RATE_LIMIT'
|
|
484
|
+
if (status === 429 || /429|rate limit|rate-?limited/i.test(detail)) return 'RATE_LIMIT'
|
|
480
485
|
if (isContextWindowExceededError(detail)) return CONTEXT_WINDOW_EXCEEDED_CODE
|
|
481
486
|
if (status === 400) return 'INVALID_REQUEST'
|
|
482
487
|
if (status !== undefined && status >= 500) return 'SERVER'
|
|
@@ -809,7 +814,9 @@ export class ClinePassAdapter extends LlmAdapter {
|
|
|
809
814
|
* nothing is left, which is when the real error is raised below.
|
|
810
815
|
*/
|
|
811
816
|
const refused = new Set()
|
|
812
|
-
|
|
817
|
+
let attemptIndex = 0
|
|
818
|
+
while (attemptIndex < attempts.length) {
|
|
819
|
+
const attempt = attempts[attemptIndex++]
|
|
813
820
|
const account = await this.config.resolveAccount({ exclude: [...refused] })
|
|
814
821
|
if (account === undefined) break
|
|
815
822
|
const body = injectPrefs(buildRequestBody(options, connection, images), meta, attempt)
|
|
@@ -865,12 +872,15 @@ export class ClinePassAdapter extends LlmAdapter {
|
|
|
865
872
|
// nothing left and this error is raised unchanged.
|
|
866
873
|
if (lastError.code === 'AUTH' || lastError.code === QUOTA_EXCEEDED_CODE) {
|
|
867
874
|
refused.add(account.name)
|
|
875
|
+
// A spent account says nothing about this channel. Give the next
|
|
876
|
+
// account the full chain, including a lone auto route or strict pin.
|
|
877
|
+
attemptIndex = 0
|
|
868
878
|
this.config.record(options.model, {
|
|
869
879
|
provider: null,
|
|
870
880
|
attempts: trace.map((row) => row.upstream),
|
|
871
881
|
ms: Date.now() - startedAt,
|
|
872
882
|
stream: true,
|
|
873
|
-
error:
|
|
883
|
+
error: lastError.message.replace(/\s+/g, ' ').trim().slice(0, 200),
|
|
874
884
|
code: lastError.code,
|
|
875
885
|
account: account.name,
|
|
876
886
|
})
|
|
@@ -939,6 +949,7 @@ export class ClinePassAdapter extends LlmAdapter {
|
|
|
939
949
|
}
|
|
940
950
|
if (code === 'AUTH' || code === QUOTA_EXCEEDED_CODE) {
|
|
941
951
|
refused.add(account.name)
|
|
952
|
+
attemptIndex = 0
|
|
942
953
|
this.config.record(options.model, {
|
|
943
954
|
provider: observed.provider,
|
|
944
955
|
canonical: observed.canonical,
|
package/lib/client.js
CHANGED
|
@@ -1544,6 +1544,19 @@ window.__ModuleLoader__.load({
|
|
|
1544
1544
|
])
|
|
1545
1545
|
}
|
|
1546
1546
|
|
|
1547
|
+
/** Select cached quota without substituting another account's reading. */
|
|
1548
|
+
function usageIndex(accounts, effectiveAccount, picked) {
|
|
1549
|
+
if (accounts.length === 0) return -1
|
|
1550
|
+
if (picked !== null) return Math.min(Math.max(picked, 0), accounts.length - 1)
|
|
1551
|
+
// Undefined is an older host; an explicit empty string means no account.
|
|
1552
|
+
if (effectiveAccount !== undefined) {
|
|
1553
|
+
if (effectiveAccount === '') return -1
|
|
1554
|
+
return accounts.findIndex((entry) => String(entry.account) === String(effectiveAccount))
|
|
1555
|
+
}
|
|
1556
|
+
const usable = accounts.findIndex((entry) => entry.ok === true)
|
|
1557
|
+
return Math.max(0, usable)
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1547
1560
|
/**
|
|
1548
1561
|
* The official quota card.
|
|
1549
1562
|
*
|
|
@@ -1567,11 +1580,10 @@ window.__ModuleLoader__.load({
|
|
|
1567
1580
|
// that may not be about to be spent. Once they page, their choice wins: the
|
|
1568
1581
|
// cursor moving underneath must not yank the card away mid-review.
|
|
1569
1582
|
const [picked, setPicked] = React.useState(null)
|
|
1570
|
-
const followed = accounts.findIndex((entry) => String(entry.account) === String(effectiveAccount ?? ''))
|
|
1571
|
-
const at = picked === null ? Math.max(0, followed) : picked
|
|
1572
1583
|
// The selection is an index, so a shorter list — an account removed, or its
|
|
1573
1584
|
// key deleted — can leave it past the end. Clamp instead of rendering nothing.
|
|
1574
|
-
const index = accounts
|
|
1585
|
+
const index = usageIndex(accounts, effectiveAccount, picked)
|
|
1586
|
+
const account = accounts[index]
|
|
1575
1587
|
const step = (delta) => setPicked((accounts.length + index + delta) % accounts.length)
|
|
1576
1588
|
const windowRow = (type, key, limit) => {
|
|
1577
1589
|
const percent = Math.max(0, Math.min(100, Number(limit.percentUsed ?? 0)))
|
|
@@ -1642,13 +1654,17 @@ window.__ModuleLoader__.load({
|
|
|
1642
1654
|
}
|
|
1643
1655
|
let body
|
|
1644
1656
|
if (!fetched) body = h('div', { key: 'wait', className: 'cp-muted' }, unavailable === true ? t('usageUnavailable') : t('usageLoading'))
|
|
1657
|
+
else if (account === undefined && effectiveAccount !== undefined) {
|
|
1658
|
+
body = h('div', { key: 'wait', className: 'cp-muted' }, effectiveAccount === '' || unavailable === true ? t('usageUnavailable') : t('usageLoading'))
|
|
1659
|
+
}
|
|
1645
1660
|
else if (accounts.length === 0) body = h('div', { key: 'none', className: 'cp-muted' }, t('usageEmpty'))
|
|
1646
1661
|
// `index` is the account on screen; its own key remounts the block so the
|
|
1647
1662
|
// windows never animate between two accounts' readings.
|
|
1648
|
-
else body = h('div', { key: 'groups', className: 'cp-usage-groups' }, accountGroup(
|
|
1663
|
+
else body = h('div', { key: 'groups', className: 'cp-usage-groups' }, accountGroup(account, index))
|
|
1664
|
+
const name = account?.account ?? effectiveAccount
|
|
1649
1665
|
const title = accounts.length > 1
|
|
1650
1666
|
? `${t('usageTitle')} · ${fill(t('accountsCount'), { n: accounts.length })}`
|
|
1651
|
-
: `${t('usageTitle')}${
|
|
1667
|
+
: `${t('usageTitle')}${name ? ` · ${fill(t('usageAccount'), { name })}` : ''}`
|
|
1652
1668
|
return h('div', { className: 'cp-card' }, [
|
|
1653
1669
|
h('div', { key: 'title', className: 'cp-card-title' }, [
|
|
1654
1670
|
h('span', { key: 'l' }, title),
|
|
@@ -1987,8 +2003,8 @@ window.__ModuleLoader__.load({
|
|
|
1987
2003
|
// Quota is per account, so showing a healthy reading from an account the
|
|
1988
2004
|
// next request will not touch is worse than showing nothing: it answers a
|
|
1989
2005
|
// question about a key that is not about to be spent. The host names the
|
|
1990
|
-
// effective account because only it knows the cursor
|
|
1991
|
-
//
|
|
2006
|
+
// effective account because only it knows the cursor. A reading missing
|
|
2007
|
+
// that account must not borrow another key's quota.
|
|
1992
2008
|
//
|
|
1993
2009
|
// The account is read from the panel's snapshot, not from this reading: a
|
|
1994
2010
|
// switch lands there at once, while a reading only arrives on a timer. The
|
|
@@ -2000,14 +2016,11 @@ window.__ModuleLoader__.load({
|
|
|
2000
2016
|
// once the user pages, their choice holds for as long as it is open.
|
|
2001
2017
|
const effectiveAccount = panel?.data?.effectiveAccount !== undefined
|
|
2002
2018
|
? panel.data.effectiveAccount
|
|
2003
|
-
:
|
|
2004
|
-
const
|
|
2005
|
-
const usable = accounts.findIndex((entry) => entry.ok === true)
|
|
2006
|
-
const at = picked === null ? (followed >= 0 ? followed : Math.max(0, usable)) : picked
|
|
2007
|
-
const index = accounts.length === 0 ? 0 : Math.min(Math.max(at, 0), accounts.length - 1)
|
|
2019
|
+
: snapshot?.value?.effectiveAccount
|
|
2020
|
+
const index = usageIndex(accounts, effectiveAccount, picked)
|
|
2008
2021
|
const account = accounts[index]
|
|
2009
2022
|
const step = (delta) => setPicked((accounts.length + index + delta) % accounts.length)
|
|
2010
|
-
const limits = account?.limits ?? []
|
|
2023
|
+
const limits = account?.ok === true ? account.limits ?? [] : []
|
|
2011
2024
|
const byType = new Map(limits.map((limit) => [String(limit.type), limit]))
|
|
2012
2025
|
const percentOf = (type) => {
|
|
2013
2026
|
const limit = byType.get(type)
|
|
@@ -2042,7 +2055,7 @@ window.__ModuleLoader__.load({
|
|
|
2042
2055
|
// it: the quota is per account, so the arrows move between them rather
|
|
2043
2056
|
// than stacking every window at once. Absent with a single account,
|
|
2044
2057
|
// where there is nowhere to go.
|
|
2045
|
-
accounts.length <= 1 ? null : h('span', { key: 'nav', className: 'cp-pill-nav' }, [
|
|
2058
|
+
accounts.length <= 1 || account === undefined ? null : h('span', { key: 'nav', className: 'cp-pill-nav' }, [
|
|
2046
2059
|
h('button', {
|
|
2047
2060
|
key: 'prev', type: 'button', className: 'cp-pill-arrow',
|
|
2048
2061
|
title: t('usagePrev'), 'aria-label': t('usagePrev'), onClick: () => step(-1),
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import z from '@deepseek-ai/schemastery'
|
|
19
19
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
20
|
-
import { assertUsableApiKey, errorChain, LlmError } from '@deepseek-ai/dsh-llm'
|
|
20
|
+
import { assertUsableApiKey, errorChain, LlmError, QUOTA_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm'
|
|
21
21
|
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
|
|
22
22
|
import {
|
|
23
23
|
ClinePassAdapter,
|
|
@@ -242,7 +242,7 @@ export function apply(ctx, config) {
|
|
|
242
242
|
* by the channel retry already, and standing an account down for it would cost
|
|
243
243
|
* capacity the request never proved was gone.
|
|
244
244
|
*/
|
|
245
|
-
const COOLDOWN_MS = {
|
|
245
|
+
const COOLDOWN_MS = { [QUOTA_EXCEEDED_CODE]: 15 * 60_000, AUTH: 30 * 60_000 }
|
|
246
246
|
|
|
247
247
|
/**
|
|
248
248
|
* Resolve the configured accounts.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cline-pass",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
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",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"LICENSE"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"test": "node test/smoke.mjs && node test/client.mjs && node test/pin-adherence.mjs",
|
|
21
|
+
"test": "node test/smoke.mjs && node test/client.mjs && node test/pin-adherence.mjs && node test/account-failover.mjs",
|
|
22
22
|
"test:protocol": "node test/pin-adherence.mjs",
|
|
23
23
|
"test:client": "node test/client.mjs",
|
|
24
24
|
"test:mount": "node test/mount.mjs",
|