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 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
- if (isQuotaExceededError(detail)) return QUOTA_EXCEEDED_CODE
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
- for (const attempt of attempts) {
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: firstLine(lastError.message, 200),
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.length === 0 ? 0 : Math.min(Math.max(at, 0), accounts.length - 1)
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(accounts[index], index))
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')}${accounts[0] ? ` · ${fill(t('usageAccount'), { name: accounts[0].account })}` : ''}`
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; the fallbacks cover
1991
- // a reading taken before that account existed.
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
- : (snapshot?.value?.effectiveAccount ?? '')
2004
- const followed = accounts.findIndex((entry) => String(entry.account) === String(effectiveAccount))
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 = { QUOTA_EXCEEDED: 15 * 60_000, AUTH: 30 * 60_000 }
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.6",
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",