dsh-cline-pass 0.2.10 → 0.2.11
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 +5 -1
- package/lib/client.js +219 -46
- package/lib/index.js +11 -0
- package/lib/panel.js +4 -0
- package/lib/protocol.js +11 -2
- package/package.json +1 -1
package/lib/adapter.js
CHANGED
|
@@ -992,7 +992,11 @@ export class ClinePassAdapter extends LlmAdapter {
|
|
|
992
992
|
: 'ok',
|
|
993
993
|
})
|
|
994
994
|
this.config.record(options.model, {
|
|
995
|
-
|
|
995
|
+
// The upstream the gateway actually served from, never the one we asked
|
|
996
|
+
// for: a pin the gateway declined is exactly what this column exists to
|
|
997
|
+
// expose, and filling it with the requested name would report the pin as if
|
|
998
|
+
// it had been adopted. An unreported routing reads as unknown upstream.
|
|
999
|
+
provider: observed.provider,
|
|
996
1000
|
canonical: observed.canonical,
|
|
997
1001
|
attempts: trace.map((row) => row.upstream),
|
|
998
1002
|
ms: Date.now() - startedAt,
|
package/lib/client.js
CHANGED
|
@@ -118,6 +118,11 @@ window.__ModuleLoader__.load({
|
|
|
118
118
|
'.cp-acct-newname{flex:0 0 150px}',
|
|
119
119
|
'.cp-acct-add,.cp-acct-key{border-top:0.5px solid var(--dsw-alias-border-l2);padding-top:10px;margin-top:2px}',
|
|
120
120
|
'.cp-chips{display:flex;flex-wrap:wrap;gap:6px}',
|
|
121
|
+
'.cp-chip-manual{border-style:dashed;color:var(--dsw-alias-label-tertiary)}',
|
|
122
|
+
'.cp-small{font-size:11.5px}',
|
|
123
|
+
'.cp-caps{flex-direction:column;gap:8px;display:flex}',
|
|
124
|
+
'.cp-caps-boxes{flex-wrap:wrap;gap:10px}',
|
|
125
|
+
'.cp-cap{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer}',
|
|
121
126
|
'.cp-chip{border:0.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:2px 8px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:5px}',
|
|
122
127
|
'.cp-chip-on{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
123
128
|
'.cp-chip-excluded{text-decoration:line-through;color:var(--dsw-alias-label-tertiary)}',
|
|
@@ -361,7 +366,8 @@ window.__ModuleLoader__.load({
|
|
|
361
366
|
historyTokensHint: '↓输入 ↑输出 ⚡缓存 🧠推理;— 表示网关本次未返回用量',
|
|
362
367
|
historyEffort: '推理强度',
|
|
363
368
|
historyAuto: '(自动)',
|
|
364
|
-
|
|
369
|
+
historyUnknown: '(未上报)',
|
|
370
|
+
noChannels: '未探测到渠道,可手动输入',
|
|
365
371
|
pinned: '已钉住',
|
|
366
372
|
auto: '自动路由',
|
|
367
373
|
modeAuto: '智能路由',
|
|
@@ -374,6 +380,19 @@ window.__ModuleLoader__.load({
|
|
|
374
380
|
reset: '恢复自动',
|
|
375
381
|
refresh: '刷新',
|
|
376
382
|
channels: '渠道(点选=钉住顺序,⊘=排除)',
|
|
383
|
+
channelPlaceholder: '输入渠道名,回车添加',
|
|
384
|
+
channelAdd: '添加',
|
|
385
|
+
channelUnverified: '未验证',
|
|
386
|
+
channelsKnown: '已探测:{list}',
|
|
387
|
+
capabilities: '上游能力',
|
|
388
|
+
capabilitiesExtraPlaceholder: '如 quantization:fp8 或 !quantization:bf16',
|
|
389
|
+
capabilitiesDirect: '最近一次探测走 OpenRouter,该路由会忽略此筛选',
|
|
390
|
+
capFree: '免费',
|
|
391
|
+
capImplicitCaching: '隐式缓存',
|
|
392
|
+
capReasoning: '推理',
|
|
393
|
+
capStructuredOutput: '结构化输出',
|
|
394
|
+
capToolUse: '工具调用',
|
|
395
|
+
capVision: '视觉',
|
|
377
396
|
history: '最近请求',
|
|
378
397
|
noHistory: '暂无记录。',
|
|
379
398
|
busy: '处理中…',
|
|
@@ -453,7 +472,8 @@ window.__ModuleLoader__.load({
|
|
|
453
472
|
historyTokensHint: '↓in ↑out ⚡cache 🧠reasoning; a dash means the gateway reported no usage',
|
|
454
473
|
historyEffort: 'effort',
|
|
455
474
|
historyAuto: '(auto)',
|
|
456
|
-
|
|
475
|
+
historyUnknown: '(unreported)',
|
|
476
|
+
noChannels: 'Nothing probed yet — type a name',
|
|
457
477
|
pinned: 'Pinned',
|
|
458
478
|
auto: 'Auto',
|
|
459
479
|
modeAuto: 'Smart routing',
|
|
@@ -466,6 +486,19 @@ window.__ModuleLoader__.load({
|
|
|
466
486
|
reset: 'Back to auto',
|
|
467
487
|
refresh: 'Refresh',
|
|
468
488
|
channels: 'Channels (click = pin order, ⊘ = exclude)',
|
|
489
|
+
channelPlaceholder: 'Channel name, Enter to add',
|
|
490
|
+
channelAdd: 'Add',
|
|
491
|
+
channelUnverified: 'unverified',
|
|
492
|
+
channelsKnown: 'Probed: {list}',
|
|
493
|
+
capabilities: 'Upstream capabilities',
|
|
494
|
+
capabilitiesExtraPlaceholder: 'e.g. quantization:fp8 or !quantization:bf16',
|
|
495
|
+
capabilitiesDirect: 'The last probe saw OpenRouter; that route ignores this filter',
|
|
496
|
+
capFree: 'Free',
|
|
497
|
+
capImplicitCaching: 'Implicit caching',
|
|
498
|
+
capReasoning: 'Reasoning',
|
|
499
|
+
capStructuredOutput: 'Structured output',
|
|
500
|
+
capToolUse: 'Tool use',
|
|
501
|
+
capVision: 'Vision',
|
|
469
502
|
history: 'Recent requests',
|
|
470
503
|
noHistory: 'Nothing recorded.',
|
|
471
504
|
busy: 'Working…',
|
|
@@ -1123,47 +1156,188 @@ window.__ModuleLoader__.load({
|
|
|
1123
1156
|
}
|
|
1124
1157
|
|
|
1125
1158
|
/** The channel chooser for one model. */
|
|
1126
|
-
|
|
1159
|
+
/** The capability names the gateway accepts, in its own spelling. */
|
|
1160
|
+
const CAPABILITIES = [
|
|
1161
|
+
['free', 'capFree'],
|
|
1162
|
+
['implicit-caching', 'capImplicitCaching'],
|
|
1163
|
+
['reasoning', 'capReasoning'],
|
|
1164
|
+
['structured-output', 'capStructuredOutput'],
|
|
1165
|
+
['tool-use', 'capToolUse'],
|
|
1166
|
+
['vision', 'capVision'],
|
|
1167
|
+
]
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* The channel list, editable.
|
|
1171
|
+
*
|
|
1172
|
+
* The chips used to be exactly what a probe returned, which made an account whose
|
|
1173
|
+
* probe came back empty a dead end: the row said no channels were probed and
|
|
1174
|
+
* offered no way to name the upstream the user already knew served them. The list
|
|
1175
|
+
* is the user's data now — a probe fills it in, typing fills it in, and both are
|
|
1176
|
+
* the same list.
|
|
1177
|
+
*/
|
|
1178
|
+
function ChannelEditor(props) {
|
|
1127
1179
|
const { t, model, disabled, onPin } = props
|
|
1128
|
-
|
|
1180
|
+
const [draft, setDraft] = React.useState('')
|
|
1129
1181
|
const verdicts = new Map((model.upstreamStatus ?? []).map((entry) => [entry.upstream, entry]))
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
},
|
|
1151
|
-
|
|
1152
|
-
upstream
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1182
|
+
const known = model.upstreams
|
|
1183
|
+
// A chosen name shows even when no probe reported it: a chip that disappears is
|
|
1184
|
+
// a chip that cannot be removed.
|
|
1185
|
+
const names = [...new Set([...known, ...model.pinned, ...model.excluded])]
|
|
1186
|
+
const pin = (next) => {
|
|
1187
|
+
if (disabled) return
|
|
1188
|
+
onPin({ model: model.id, upstreams: next })
|
|
1189
|
+
}
|
|
1190
|
+
const add = () => {
|
|
1191
|
+
const name = draft.trim()
|
|
1192
|
+
if (name === '') return
|
|
1193
|
+
setDraft('')
|
|
1194
|
+
const needle = name.toLowerCase()
|
|
1195
|
+
if (model.pinned.some((entry) => String(entry).toLowerCase() === needle)) return
|
|
1196
|
+
pin([...model.pinned, name])
|
|
1197
|
+
}
|
|
1198
|
+
return h('div', { className: 'cp-caps' }, [
|
|
1199
|
+
h('div', { key: 'row', className: 'cp-row' }, [
|
|
1200
|
+
h('span', { key: 'label', className: 'cp-muted' }, t('channels')),
|
|
1201
|
+
names.length === 0
|
|
1202
|
+
? h('span', { key: 'none', className: 'cp-muted' }, t('noChannels'))
|
|
1203
|
+
: h('span', { key: 'chips', className: 'cp-chips' }, names.map((upstream) => {
|
|
1204
|
+
const pinned = model.pinned.includes(upstream)
|
|
1205
|
+
const excluded = model.excluded.includes(upstream)
|
|
1206
|
+
const probed = known.includes(upstream)
|
|
1207
|
+
const verdict = verdicts.get(upstream)
|
|
1208
|
+
const detail = verdict === undefined
|
|
1209
|
+
? ''
|
|
1210
|
+
: ` · ${VERDICT[verdict.status] ?? verdict.status}${verdict.note ? `\n${verdict.note}` : ''}`
|
|
1211
|
+
return h('span', {
|
|
1212
|
+
key: upstream,
|
|
1213
|
+
className: `cp-chip${pinned ? ' cp-chip-on' : ''}${excluded ? ' cp-chip-excluded' : ''}${probed ? '' : ' cp-chip-manual'}`,
|
|
1214
|
+
}, [
|
|
1215
|
+
h('span', {
|
|
1216
|
+
key: 'pin',
|
|
1217
|
+
role: 'button',
|
|
1218
|
+
tabIndex: 0,
|
|
1219
|
+
title: `${upstream}${probed ? '' : ` · ${t('channelUnverified')}`}${detail}`,
|
|
1220
|
+
onClick: () => {
|
|
1221
|
+
if (disabled) return
|
|
1222
|
+
const next = pinned ? model.pinned.filter((name) => name !== upstream) : [...model.pinned, upstream]
|
|
1223
|
+
onPin({ model: model.id, upstreams: next })
|
|
1224
|
+
},
|
|
1225
|
+
}, [
|
|
1226
|
+
h('span', { key: 'dot', style: { marginRight: 5 } }, h(Dot, { color: verdictColor(verdict?.status ?? 'unknown'), title: VERDICT[verdict?.status ?? 'unknown'] })),
|
|
1227
|
+
upstream,
|
|
1228
|
+
]),
|
|
1229
|
+
h('span', {
|
|
1230
|
+
key: 'ex',
|
|
1231
|
+
role: 'button',
|
|
1232
|
+
tabIndex: 0,
|
|
1233
|
+
title: excluded ? 'un-exclude' : 'exclude',
|
|
1234
|
+
onClick: () => {
|
|
1235
|
+
if (disabled) return
|
|
1236
|
+
const next = excluded ? model.excluded.filter((name) => name !== upstream) : [...model.excluded, upstream]
|
|
1237
|
+
onPin({ model: model.id, exclude: next })
|
|
1238
|
+
},
|
|
1239
|
+
}, '⊘'),
|
|
1240
|
+
// A name that only exists because someone typed it has no discovery
|
|
1241
|
+
// list to fall back on, so it needs an explicit way out: un-pinning
|
|
1242
|
+
// removes it, and this is what says so.
|
|
1243
|
+
probed ? null : h('span', {
|
|
1244
|
+
key: 'del',
|
|
1245
|
+
role: 'button',
|
|
1246
|
+
tabIndex: 0,
|
|
1247
|
+
title: 'remove',
|
|
1248
|
+
onClick: () => {
|
|
1249
|
+
if (disabled) return
|
|
1250
|
+
onPin({
|
|
1251
|
+
model: model.id,
|
|
1252
|
+
upstreams: model.pinned.filter((name) => name !== upstream),
|
|
1253
|
+
exclude: model.excluded.filter((name) => name !== upstream),
|
|
1254
|
+
})
|
|
1255
|
+
},
|
|
1256
|
+
}, '×'),
|
|
1257
|
+
])
|
|
1258
|
+
})),
|
|
1259
|
+
]),
|
|
1260
|
+
h('div', { key: 'add', className: 'cp-row' }, [
|
|
1261
|
+
h('input', {
|
|
1262
|
+
key: 'in',
|
|
1263
|
+
type: 'text',
|
|
1264
|
+
className: 'cp-input cp-grow',
|
|
1265
|
+
value: draft,
|
|
1266
|
+
disabled,
|
|
1267
|
+
placeholder: t('channelPlaceholder'),
|
|
1268
|
+
'aria-label': `${t('channelAdd')}: ${model.id}`,
|
|
1269
|
+
onChange: (event) => setDraft(event.target.value),
|
|
1270
|
+
onKeyDown: (event) => {
|
|
1271
|
+
if (event.key !== 'Enter') return
|
|
1272
|
+
event.preventDefault()
|
|
1273
|
+
add()
|
|
1274
|
+
},
|
|
1275
|
+
}),
|
|
1276
|
+
h('button', { key: 'b', type: 'button', className: 'cp-btn', disabled, onClick: add }, t('channelAdd')),
|
|
1277
|
+
]),
|
|
1278
|
+
known.length === 0 ? null : h('div', { key: 'known', className: 'cp-muted cp-small' }, fill(t('channelsKnown'), { list: known.join(' · ') })),
|
|
1279
|
+
])
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* The capabilities an upstream must offer for this model.
|
|
1284
|
+
*
|
|
1285
|
+
* The gateway applies this itself and rejects a name it does not know, so the six
|
|
1286
|
+
* it documents are checkboxes and anything else — a weight format such as
|
|
1287
|
+
* `quantization:fp8`, or its negation — is typed. It only exists on the planner
|
|
1288
|
+
* pipeline: OpenRouter has no equivalent, and a model known to run there says so
|
|
1289
|
+
* rather than letting a filter look active while it is ignored.
|
|
1290
|
+
*/
|
|
1291
|
+
function CapabilityFilter(props) {
|
|
1292
|
+
const { t, model, disabled, onPin } = props
|
|
1293
|
+
const [extra, setExtra] = React.useState('')
|
|
1294
|
+
const chosen = model.has ?? []
|
|
1295
|
+
const write = (next) => {
|
|
1296
|
+
if (disabled) return
|
|
1297
|
+
onPin({ model: model.id, has: next })
|
|
1298
|
+
}
|
|
1299
|
+
const toggle = (id) => write(chosen.includes(id) ? chosen.filter((name) => name !== id) : [...chosen, id])
|
|
1300
|
+
const addExtra = () => {
|
|
1301
|
+
const value = extra.trim()
|
|
1302
|
+
if (value === '') return
|
|
1303
|
+
setExtra('')
|
|
1304
|
+
if (chosen.includes(value)) return
|
|
1305
|
+
write([...chosen, value])
|
|
1306
|
+
}
|
|
1307
|
+
const extras = chosen.filter((name) => !CAPABILITIES.some(([id]) => id === name))
|
|
1308
|
+
return h('div', { className: 'cp-caps' }, [
|
|
1309
|
+
h('div', { key: 'head', className: 'cp-row' }, [
|
|
1310
|
+
h('span', { key: 'l', className: 'cp-muted' }, t('capabilities')),
|
|
1311
|
+
model.pipeline === 'direct' ? h('span', { key: 'w', className: 'cp-muted cp-small' }, t('capabilitiesDirect')) : null,
|
|
1312
|
+
]),
|
|
1313
|
+
h('span', { key: 'boxes', className: 'cp-row cp-caps-boxes' }, CAPABILITIES.map(([id, key]) =>
|
|
1314
|
+
h('label', { key: id, className: 'cp-cap' }, [
|
|
1315
|
+
h('input', {
|
|
1316
|
+
key: 'c', type: 'checkbox', className: 'cp-check',
|
|
1317
|
+
checked: chosen.includes(id), disabled, onChange: () => toggle(id),
|
|
1318
|
+
}),
|
|
1319
|
+
h('span', { key: 't' }, t(key)),
|
|
1320
|
+
]))),
|
|
1321
|
+
h('div', { key: 'extra', className: 'cp-row' }, [
|
|
1322
|
+
h('input', {
|
|
1323
|
+
key: 'in', type: 'text', className: 'cp-input cp-grow',
|
|
1324
|
+
value: extra, disabled,
|
|
1325
|
+
placeholder: t('capabilitiesExtraPlaceholder'),
|
|
1326
|
+
'aria-label': `${t('capabilities')}: ${model.id}`,
|
|
1327
|
+
onChange: (event) => setExtra(event.target.value),
|
|
1328
|
+
onKeyDown: (event) => {
|
|
1329
|
+
if (event.key !== 'Enter') return
|
|
1330
|
+
event.preventDefault()
|
|
1331
|
+
addExtra()
|
|
1332
|
+
},
|
|
1333
|
+
}),
|
|
1334
|
+
h('button', { key: 'b', type: 'button', className: 'cp-btn', disabled, onClick: addExtra }, t('channelAdd')),
|
|
1335
|
+
]),
|
|
1336
|
+
extras.length === 0 ? null : h('span', { key: 'list', className: 'cp-chips' }, extras.map((name) =>
|
|
1337
|
+
h('span', { key: name, className: 'cp-chip cp-chip-on' }, [
|
|
1338
|
+
h('span', { key: 'n' }, name),
|
|
1339
|
+
h('span', { key: 'x', role: 'button', tabIndex: 0, title: 'remove', onClick: () => toggle(name) }, '×'),
|
|
1340
|
+
]))),
|
|
1167
1341
|
])
|
|
1168
1342
|
}
|
|
1169
1343
|
|
|
@@ -1230,9 +1404,8 @@ window.__ModuleLoader__.load({
|
|
|
1230
1404
|
action('test', t('testPin'), actions.testModel),
|
|
1231
1405
|
action('reset', t('reset'), actions.resetModel),
|
|
1232
1406
|
]),
|
|
1233
|
-
model.
|
|
1234
|
-
|
|
1235
|
-
: h('div', { key: 'none', className: 'cp-muted' }, t('noChannels')),
|
|
1407
|
+
h(ChannelEditor, { key: 'channels', t, model, disabled, onPin: (payload) => actions.pinModel(payload) }),
|
|
1408
|
+
h(CapabilityFilter, { key: 'caps', t, model, disabled, onPin: (payload) => actions.pinModel(payload) }),
|
|
1236
1409
|
]) : null,
|
|
1237
1410
|
])
|
|
1238
1411
|
}
|
|
@@ -1487,7 +1660,7 @@ window.__ModuleLoader__.load({
|
|
|
1487
1660
|
*/
|
|
1488
1661
|
function historyRows(t, entry, showAccount) {
|
|
1489
1662
|
const failed = String(entry.error ?? '') !== ''
|
|
1490
|
-
const upstream = `${entry.provider || t('
|
|
1663
|
+
const upstream = `${entry.provider || t('historyUnknown')}${showAccount && entry.account !== '' ? ` · ${entry.account}` : ''}`
|
|
1491
1664
|
// The route prefix is constant across every row of this list, so it is
|
|
1492
1665
|
// carried in the tooltip instead of eating the width the model name needs
|
|
1493
1666
|
// — centred text that overflows loses BOTH ends, not just the tail.
|
|
@@ -1516,7 +1689,7 @@ window.__ModuleLoader__.load({
|
|
|
1516
1689
|
h('td', { key: 'model', className: 'cp-history-model-cell' }, h('div', { className: 'cp-history-model-stack' }, [
|
|
1517
1690
|
h('span', { key: 'name', className: 'cp-history-model', title: entry.model }, label),
|
|
1518
1691
|
h('span', { key: 'upstream', className: 'cp-history-meta' }, [
|
|
1519
|
-
h('span', { key: 'provider', className: 'cp-tag' }, entry.provider || t('
|
|
1692
|
+
h('span', { key: 'provider', className: 'cp-tag' }, entry.provider || t('historyUnknown')),
|
|
1520
1693
|
showAccount && entry.account !== '' ? h('span', { key: 'account', className: 'cp-tag cp-tag-quiet' }, entry.account) : null,
|
|
1521
1694
|
]),
|
|
1522
1695
|
])),
|
package/lib/index.js
CHANGED
|
@@ -106,6 +106,17 @@ export const PinProfile = z.object({
|
|
|
106
106
|
exclude: z.array(z.string()).default([]),
|
|
107
107
|
pinMode: z.string().default('strict'),
|
|
108
108
|
sort: z.string().default(''),
|
|
109
|
+
/**
|
|
110
|
+
* Capabilities an upstream must offer for this model, in the gateway's own
|
|
111
|
+
* vocabulary: `free`, `implicit-caching`, `reasoning`, `structured-output`,
|
|
112
|
+
* `tool-use`, `vision`, or a weight condition such as `quantization:fp8` /
|
|
113
|
+
* `!quantization:bf16`.
|
|
114
|
+
*
|
|
115
|
+
* The gateway applies this server-side and rejects a name it does not know, so
|
|
116
|
+
* the panel checks the list before it is written. It only exists on the planner
|
|
117
|
+
* pipeline — OpenRouter has no equivalent — and a model routed there ignores it.
|
|
118
|
+
*/
|
|
119
|
+
has: z.array(z.string()).default([]),
|
|
109
120
|
})
|
|
110
121
|
|
|
111
122
|
/**
|
package/lib/panel.js
CHANGED
|
@@ -71,6 +71,7 @@ export function projectModel(model, pin, meta, hidden = false) {
|
|
|
71
71
|
sort: String(pin?.sort ?? ''),
|
|
72
72
|
pinned: Array.isArray(pin?.upstreams) ? pin.upstreams.map(String) : [],
|
|
73
73
|
excluded: Array.isArray(pin?.exclude) ? pin.exclude.map(String) : [],
|
|
74
|
+
has: Array.isArray(pin?.has) ? pin.has.map(String) : [],
|
|
74
75
|
upstreams: Array.isArray(meta?.upstreams) ? meta.upstreams.map(String) : [],
|
|
75
76
|
upstreamStatus,
|
|
76
77
|
lastProvider: String(meta?.lastProvider ?? ''),
|
|
@@ -377,6 +378,9 @@ export function createPanel({ control, engine, store }) {
|
|
|
377
378
|
sort: payload?.sort === undefined
|
|
378
379
|
? (current.sort ?? '')
|
|
379
380
|
: (payload.sort === 'none' || payload.sort === '' ? '' : String(payload.sort)),
|
|
381
|
+
has: Array.isArray(payload?.has)
|
|
382
|
+
? [...new Set(payload.has.map((name) => String(name).trim()).filter((name) => name.length > 0))]
|
|
383
|
+
: (current.has ?? []).map(String),
|
|
380
384
|
}
|
|
381
385
|
await control.updateConfig({ perModel: { ...(config.perModel ?? {}), [model]: next } })
|
|
382
386
|
return { model, pin: next, ...(await state()) }
|
package/lib/protocol.js
CHANGED
|
@@ -184,6 +184,9 @@ function knownChannels(meta, pipeline) {
|
|
|
184
184
|
export function injectPrefs(body, meta, attempt) {
|
|
185
185
|
const next = { ...body }
|
|
186
186
|
const { upstream = null, orderRest = [], excludeList = [], strict = true } = attempt ?? {}
|
|
187
|
+
// `has` names the capabilities an upstream must offer. There is no equivalent on
|
|
188
|
+
// the OpenRouter spelling, so a direct-pipeline model simply does not carry it.
|
|
189
|
+
const required = (Array.isArray(attempt?.has) ? attempt.has : []).map((name) => String(name).trim()).filter((name) => name.length > 0)
|
|
187
190
|
// Normalized here as well as in buildAttempts: this function is exported, so
|
|
188
191
|
// a caller can hand it an attempt straight from configuration.
|
|
189
192
|
const sort = normalizeSort(attempt?.sort)
|
|
@@ -197,7 +200,7 @@ export function injectPrefs(body, meta, attempt) {
|
|
|
197
200
|
const both = attempt?.spelling !== 'detected'
|
|
198
201
|
const useGateway = both || pipeline === 'planner' || pipeline === null
|
|
199
202
|
const useOpenRouter = both || pipeline === 'direct' || pipeline === null
|
|
200
|
-
const gatewayUsable = useGateway && (upstream !== null || sort !== null || (gatewayAllow !== null && gatewayAllow.length > 0))
|
|
203
|
+
const gatewayUsable = useGateway && (upstream !== null || sort !== null || required.length > 0 || (gatewayAllow !== null && gatewayAllow.length > 0))
|
|
201
204
|
// `ignore` is OpenRouter's own exclusion field: written alongside the
|
|
202
205
|
// allow-list so an exclusion still bites when the allow-list cannot be
|
|
203
206
|
// computed (no channel list yet) or has gone stale.
|
|
@@ -215,6 +218,7 @@ export function injectPrefs(body, meta, attempt) {
|
|
|
215
218
|
gateway.only = spell(gatewayAllow, 'planner')
|
|
216
219
|
}
|
|
217
220
|
if (sort !== null) gateway.sort = sort
|
|
221
|
+
if (required.length > 0) gateway.has = required
|
|
218
222
|
next.providerOptions = { ...(next.providerOptions ?? {}), gateway: { ...(next.providerOptions?.gateway ?? {}), ...gateway } }
|
|
219
223
|
}
|
|
220
224
|
if (openRouterUsable) {
|
|
@@ -316,7 +320,12 @@ export function buildAttempts(config) {
|
|
|
316
320
|
const wanted = listed.filter((name) => !excluded.has(normalizeSlug(name)))
|
|
317
321
|
const strict = normalizePinMode(config?.pinMode) === 'strict'
|
|
318
322
|
const sort = normalizeSort(config?.sort)
|
|
319
|
-
|
|
323
|
+
// The capability filter is gateway-only and applies to every attempt, so it rides
|
|
324
|
+
// in the shared base rather than being threaded through each candidate.
|
|
325
|
+
const has = (Array.isArray(config?.has) ? config.has : [])
|
|
326
|
+
.map((name) => String(name).trim())
|
|
327
|
+
.filter((name) => name.length > 0)
|
|
328
|
+
const base = { strict, sort, excludeList: exclude, ...(has.length > 0 ? { has } : {}) }
|
|
320
329
|
// Carried through only when the caller configured it, so an attempt built from
|
|
321
330
|
// a bare pin profile keeps its historical shape.
|
|
322
331
|
if (typeof config?.spelling === 'string' && config.spelling.length > 0) base.spelling = config.spelling
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cline-pass",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
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",
|