dsh-cline-pass 0.2.7 → 0.2.9
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 +38 -86
- package/README.md +35 -87
- package/lib/accounts.js +31 -0
- package/lib/adapter.js +7 -3
- package/lib/catalog.js +28 -1
- package/lib/client.js +24 -22
- package/lib/cline.js +93 -2
- package/lib/compat.js +28 -1
- package/lib/engine.js +22 -14
- package/lib/index.js +27 -24
- package/lib/panel.js +12 -15
- package/lib/protocol.js +17 -9
- package/lib/store.js +9 -3
- package/lib/tools.js +21 -19
- package/package.json +2 -2
package/lib/protocol.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* log instead, which turns "did the patched code actually load?" into a
|
|
20
20
|
* question with an answer.
|
|
21
21
|
*/
|
|
22
|
-
export const BUILD_TAG = 'dshfix-
|
|
22
|
+
export const BUILD_TAG = 'dshfix-4'
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* The gateway wraps some answers in a `{ data: … }` envelope; unwrap it.
|
|
@@ -45,6 +45,12 @@ export function errorText(value) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Account authentication refusals, including responses with no useful body. */
|
|
49
|
+
export function isAuthenticationError(message, status) {
|
|
50
|
+
const detail = String(message ?? '').replace(/[_-]/g, ' ')
|
|
51
|
+
return status === 401 || status === 403 || /unauthorized|re\s*authenticate|invalid\s*(?:api\s*key|token)|\b401\b|\b403\b/i.test(detail)
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
/**
|
|
49
55
|
* Read the routing facts out of one completed gateway response.
|
|
50
56
|
* @param json - raw decoded response body (enveloped or not).
|
|
@@ -181,11 +187,12 @@ export function injectPrefs(body, meta, attempt) {
|
|
|
181
187
|
// Normalized here as well as in buildAttempts: this function is exported, so
|
|
182
188
|
// a caller can hand it an attempt straight from configuration.
|
|
183
189
|
const sort = normalizeSort(attempt?.sort)
|
|
184
|
-
const excluded = excludeList.filter((name) => name !== upstream)
|
|
190
|
+
const excluded = excludeList.filter((name) => normalizeSlug(name) !== normalizeSlug(upstream ?? ''))
|
|
191
|
+
const excludedIds = new Set(excluded.map(normalizeSlug))
|
|
185
192
|
// Each spelling is written in the vocabulary its own router understands.
|
|
186
193
|
const spell = (names, pipeline) => names.map((name) => channelNameFor(name, pipeline))
|
|
187
|
-
const gatewayAllow = excluded.length > 0 ? knownChannels(meta, 'planner').filter((name) => !
|
|
188
|
-
const openRouterAllow = excluded.length > 0 ? knownChannels(meta, 'direct').filter((name) => !
|
|
194
|
+
const gatewayAllow = excluded.length > 0 ? knownChannels(meta, 'planner').filter((name) => !excludedIds.has(normalizeSlug(name))) : null
|
|
195
|
+
const openRouterAllow = excluded.length > 0 ? knownChannels(meta, 'direct').filter((name) => !excludedIds.has(normalizeSlug(name))) : null
|
|
189
196
|
const pipeline = meta?.pipeline ?? null
|
|
190
197
|
const both = attempt?.spelling !== 'detected'
|
|
191
198
|
const useGateway = both || pipeline === 'planner' || pipeline === null
|
|
@@ -278,13 +285,14 @@ export function isAdherenceOk(adherence) {
|
|
|
278
285
|
*/
|
|
279
286
|
export function pinWarnings(attempt, meta) {
|
|
280
287
|
const upstream = attempt?.upstream ?? null
|
|
281
|
-
const excluded = (attempt?.excludeList ?? []).filter((name) => name !== upstream)
|
|
288
|
+
const excluded = (attempt?.excludeList ?? []).filter((name) => normalizeSlug(name) !== normalizeSlug(upstream ?? ''))
|
|
289
|
+
const excludedIds = new Set(excluded.map(normalizeSlug))
|
|
282
290
|
if (excluded.length === 0) return []
|
|
283
291
|
const known = Array.isArray(meta?.upstreams) ? meta.upstreams : []
|
|
284
292
|
if (known.length === 0) {
|
|
285
293
|
return [`excludeUnresolved: this model has no channel list yet, so ${excluded.join(', ')} could not be compiled into an allow-list — run cline_pass_probe for it`]
|
|
286
294
|
}
|
|
287
|
-
const remaining = known.filter((name) => !
|
|
295
|
+
const remaining = known.filter((name) => !excludedIds.has(normalizeSlug(name)))
|
|
288
296
|
if (remaining.length === 0 && upstream === null) {
|
|
289
297
|
return [`PIN_UNRESOLVED: every known channel is excluded (${known.join(', ')}), so the request falls back to the gateway's own routing`]
|
|
290
298
|
}
|
|
@@ -304,8 +312,8 @@ export function pinWarnings(attempt, meta) {
|
|
|
304
312
|
export function buildAttempts(config) {
|
|
305
313
|
const listed = (config?.upstreams ?? []).filter((name) => typeof name === 'string' && name.length > 0)
|
|
306
314
|
const exclude = (config?.exclude ?? []).filter((name) => typeof name === 'string' && name.length > 0)
|
|
307
|
-
const excluded = new Set(exclude)
|
|
308
|
-
const wanted = listed.filter((name) => !excluded.has(name))
|
|
315
|
+
const excluded = new Set(exclude.map(normalizeSlug))
|
|
316
|
+
const wanted = listed.filter((name) => !excluded.has(normalizeSlug(name)))
|
|
309
317
|
const strict = normalizePinMode(config?.pinMode) === 'strict'
|
|
310
318
|
const sort = normalizeSort(config?.sort)
|
|
311
319
|
const base = { strict, sort, excludeList: exclude }
|
|
@@ -332,7 +340,7 @@ export function classifyUpstreamError(message) {
|
|
|
332
340
|
if (/empty response content/i.test(text)) return 'ok'
|
|
333
341
|
if (/429|rate-?limited|temporarily rate/i.test(text)) return 'limited'
|
|
334
342
|
if (/invalid_request|modelid|no allowed providers|no available providers|not found|unsupported/i.test(text)) return 'bad'
|
|
335
|
-
if (
|
|
343
|
+
if (isAuthenticationError(text)) return 'auth'
|
|
336
344
|
return 'unknown'
|
|
337
345
|
}
|
|
338
346
|
|
package/lib/store.js
CHANGED
|
@@ -26,6 +26,7 @@ export function createStore({ historyLimit = DEFAULT_HISTORY_LIMIT } = {}) {
|
|
|
26
26
|
const models = new Map()
|
|
27
27
|
/** @type {object[]} newest first */
|
|
28
28
|
const history = []
|
|
29
|
+
let historyRevision = 0
|
|
29
30
|
let catalog = { ids: [], sources: [], fetchedAt: 0 }
|
|
30
31
|
/** Last quota reading, one entry per enabled account. */
|
|
31
32
|
let usage = { ok: false, accounts: [], fetchedAt: 0 }
|
|
@@ -55,10 +56,10 @@ export function createStore({ historyLimit = DEFAULT_HISTORY_LIMIT } = {}) {
|
|
|
55
56
|
models.set(model, next)
|
|
56
57
|
return next
|
|
57
58
|
},
|
|
58
|
-
/** Record
|
|
59
|
-
learnUpstream(model, upstream, status, note, ms) {
|
|
59
|
+
/** Record a verdict; explicit validation also replaces stale results with unknown. */
|
|
60
|
+
learnUpstream(model, upstream, status, note, ms, { includeUnknown = false } = {}) {
|
|
60
61
|
if (upstream === undefined || upstream === null || upstream === '') return
|
|
61
|
-
if (status === 'unknown') return
|
|
62
|
+
if (status === 'unknown' && !includeUnknown) return
|
|
62
63
|
const current = models.get(model) ?? {}
|
|
63
64
|
models.set(model, {
|
|
64
65
|
...current,
|
|
@@ -67,6 +68,7 @@ export function createStore({ historyLimit = DEFAULT_HISTORY_LIMIT } = {}) {
|
|
|
67
68
|
},
|
|
68
69
|
/** Append one request-history row (newest first, bounded). */
|
|
69
70
|
record(entry) {
|
|
71
|
+
historyRevision += 1
|
|
70
72
|
history.unshift({ ts: Date.now(), ...entry })
|
|
71
73
|
if (history.length > historyLimit) history.length = historyLimit
|
|
72
74
|
},
|
|
@@ -85,6 +87,10 @@ export function createStore({ historyLimit = DEFAULT_HISTORY_LIMIT } = {}) {
|
|
|
85
87
|
historySize() {
|
|
86
88
|
return history.length
|
|
87
89
|
},
|
|
90
|
+
/** Monotonic change counter, including replacements at the retention cap. */
|
|
91
|
+
historyRevision() {
|
|
92
|
+
return historyRevision
|
|
93
|
+
},
|
|
88
94
|
/** The last cached official-catalog scan. */
|
|
89
95
|
catalog() {
|
|
90
96
|
return catalog
|
package/lib/tools.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module dsh-cline-pass/tools
|
|
8
8
|
*/
|
|
9
9
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
10
|
+
import { accountProfileFor } from './accounts.js'
|
|
10
11
|
import { BUILD_TAG, normalizePinMode, pinWarnings } from './protocol.js'
|
|
11
12
|
|
|
12
13
|
/** One model-facing text block. */
|
|
@@ -625,6 +626,7 @@ export function registerTools(ctx, { control, engine, store }) {
|
|
|
625
626
|
apiKeyEnv: { type: 'string', description: 'Credential name holding this account key.' },
|
|
626
627
|
displayName: { type: 'string', description: 'Human-readable label.' },
|
|
627
628
|
enabled: { type: 'boolean', description: 'Whether the account takes part in routing.' },
|
|
629
|
+
baseURL: { type: 'string', description: 'Override the gateway address for this account; empty uses the provider address.' },
|
|
628
630
|
mode: { type: 'string', enum: ['single', 'roundrobin'], description: 'Pool mode.' },
|
|
629
631
|
active: { type: 'string', description: 'Account key used in single mode.' },
|
|
630
632
|
accounts: {
|
|
@@ -638,6 +640,7 @@ export function registerTools(ctx, { control, engine, store }) {
|
|
|
638
640
|
apiKeyEnv: { type: 'string' },
|
|
639
641
|
displayName: { type: 'string' },
|
|
640
642
|
enabled: { type: 'boolean' },
|
|
643
|
+
baseURL: { type: 'string' },
|
|
641
644
|
key: { type: 'string' },
|
|
642
645
|
},
|
|
643
646
|
},
|
|
@@ -667,21 +670,22 @@ export function registerTools(ctx, { control, engine, store }) {
|
|
|
667
670
|
async execute(args, exec) {
|
|
668
671
|
const action = String(args.action)
|
|
669
672
|
let note = ''
|
|
673
|
+
let ok = true
|
|
670
674
|
const nameOf = (value) => String(value ?? '').trim()
|
|
671
675
|
if (action === 'test') {
|
|
672
676
|
let key = String(args.key ?? '').trim()
|
|
673
677
|
let label = args.name === undefined ? '(supplied key)' : nameOf(args.name)
|
|
674
|
-
|
|
678
|
+
const account = args.name === undefined ? undefined : control.accounts().find((entry) => entry.key === nameOf(args.name))
|
|
679
|
+
if (args.name !== undefined && account === undefined) throw new Error(`no account named ${JSON.stringify(nameOf(args.name))}`)
|
|
680
|
+
const baseURL = account?.baseURL || readConfig().baseURL
|
|
675
681
|
if (key === '') {
|
|
676
682
|
if (args.name === undefined) throw new Error('cline_pass_accounts action="test" needs `key` or an existing `name`')
|
|
677
|
-
const account = control.accounts().find((entry) => entry.key === nameOf(args.name))
|
|
678
|
-
if (account === undefined) throw new Error(`no account named ${JSON.stringify(nameOf(args.name))}`)
|
|
679
683
|
key = await control.readCredential(account.apiKeyEnv)
|
|
680
684
|
if (key === '') throw new Error(`account ${JSON.stringify(account.key)} has no stored key at ${account.apiKeyEnv}; store one with action="add" key=… or through the dsh Models page`)
|
|
681
|
-
baseURL = account.baseURL || baseURL
|
|
682
685
|
}
|
|
683
686
|
const model = (readConfig().knownModels ?? [])[0] ?? 'cline-pass/glm-5.3-flash'
|
|
684
687
|
const result = await engine.testAccount({ key, baseURL, model, signal: exec.signal })
|
|
688
|
+
ok = result.authorized === true
|
|
685
689
|
note = result.authorized
|
|
686
690
|
? `key for ${label} authorized in ${result.ms}ms`
|
|
687
691
|
: `key for ${label} FAILED: ${firstLine(result.error, 200)}`
|
|
@@ -693,18 +697,20 @@ export function registerTools(ctx, { control, engine, store }) {
|
|
|
693
697
|
const next = {}
|
|
694
698
|
for (const [index, entry] of args.accounts.entries()) {
|
|
695
699
|
const key = nameOf(entry?.name) || `account-${index + 1}`
|
|
696
|
-
next
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
700
|
+
if (Object.hasOwn(next, key)) throw new Error(`duplicate account name ${JSON.stringify(key)}`)
|
|
701
|
+
next[key] = accountProfileFor(key, entry, { ...accounts, ...next }, config.apiKeyEnv)
|
|
702
|
+
}
|
|
703
|
+
// Validate every entry before any credential or configuration write.
|
|
704
|
+
for (const [index, entry] of args.accounts.entries()) {
|
|
705
|
+
const key = nameOf(entry?.name) || `account-${index + 1}`
|
|
702
706
|
if (typeof entry?.key === 'string' && entry.key.trim() !== '') {
|
|
703
707
|
await control.setCredential(next[key].apiKeyEnv, entry.key.trim())
|
|
704
708
|
note = `stored the key for ${key} in the credential store`
|
|
705
709
|
}
|
|
706
710
|
}
|
|
707
|
-
|
|
711
|
+
const removals = Object.keys(accounts).filter((key) => !Object.hasOwn(next, key)).map((key) => ['accounts', key])
|
|
712
|
+
const activeAccount = Object.hasOwn(next, String(config.activeAccount ?? '')) ? config.activeAccount : ''
|
|
713
|
+
await control.updateConfig({ accounts: next, activeAccount }, removals)
|
|
708
714
|
} else if (action === 'add') {
|
|
709
715
|
const key = nameOf(args.name)
|
|
710
716
|
if (!/^[A-Za-z0-9._-]+$/.test(key)) throw new Error('cline_pass_accounts action="add" needs a `name` of letters, digits, dot, dash or underscore')
|
|
@@ -719,13 +725,9 @@ export function registerTools(ctx, { control, engine, store }) {
|
|
|
719
725
|
baseURL: '',
|
|
720
726
|
}
|
|
721
727
|
}
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
apiKeyEnv,
|
|
726
|
-
enabled: args.enabled !== false,
|
|
727
|
-
baseURL: '',
|
|
728
|
-
}
|
|
728
|
+
const profile = accountProfileFor(key, args, accounts, config.apiKeyEnv)
|
|
729
|
+
const apiKeyEnv = profile.apiKeyEnv
|
|
730
|
+
accounts[key] = profile
|
|
729
731
|
await control.updateConfig({ accounts })
|
|
730
732
|
if (typeof args.key === 'string' && args.key.trim() !== '') {
|
|
731
733
|
await control.setCredential(apiKeyEnv, args.key.trim())
|
|
@@ -762,7 +764,7 @@ export function registerTools(ctx, { control, engine, store }) {
|
|
|
762
764
|
}
|
|
763
765
|
const config = readConfig()
|
|
764
766
|
return {
|
|
765
|
-
ok
|
|
767
|
+
ok,
|
|
766
768
|
action,
|
|
767
769
|
note,
|
|
768
770
|
accountMode: config.accountMode === 'roundrobin' ? 'roundrobin' : 'single',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cline-pass",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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 && node test/account-failover.mjs",
|
|
21
|
+
"test": "node test/smoke.mjs && node test/client.mjs && node test/pin-adherence.mjs && node test/account-failover.mjs && node test/review-regressions.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",
|