dsh-cline-pass 0.2.10 → 0.2.12
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 +6 -12
- package/lib/catalog.js +21 -2
- package/lib/client.js +304 -49
- package/lib/index.js +16 -0
- package/lib/panel.js +41 -0
- package/lib/protocol.js +20 -3
- package/package.json +1 -1
package/lib/adapter.js
CHANGED
|
@@ -54,17 +54,7 @@ export const DEFAULT_MAX_CONCURRENT_REQUESTS = 16
|
|
|
54
54
|
* unknown ones; raise it per model in configuration when a new model ships.
|
|
55
55
|
*/
|
|
56
56
|
export const DEFAULT_CONTEXT_WINDOW = 128000
|
|
57
|
-
|
|
58
|
-
* Per-request output budget for a model whose caller names no `maxTokens`.
|
|
59
|
-
*
|
|
60
|
-
* A budget, not a ceiling: the harness materializes it into requests that omit
|
|
61
|
-
* `maxTokens` and reserves the same number as completion room before it compacts,
|
|
62
|
-
* so this is the number that decides how much of the window stays available to the
|
|
63
|
-
* conversation. 64k is far past any single response a tool-calling agent emits
|
|
64
|
-
* while keeping the reserve well under the point where it would start displacing
|
|
65
|
-
* the compaction ratio. The published ceiling clamps it per model.
|
|
66
|
-
*/
|
|
67
|
-
export const DEFAULT_MAX_TOKENS = 64000
|
|
57
|
+
export { DEFAULT_MAX_TOKENS } from './catalog.js'
|
|
68
58
|
/**
|
|
69
59
|
* Request-image budget applied when the caller names none.
|
|
70
60
|
*
|
|
@@ -992,7 +982,11 @@ export class ClinePassAdapter extends LlmAdapter {
|
|
|
992
982
|
: 'ok',
|
|
993
983
|
})
|
|
994
984
|
this.config.record(options.model, {
|
|
995
|
-
|
|
985
|
+
// The upstream the gateway actually served from, never the one we asked
|
|
986
|
+
// for: a pin the gateway declined is exactly what this column exists to
|
|
987
|
+
// expose, and filling it with the requested name would report the pin as if
|
|
988
|
+
// it had been adopted. An unreported routing reads as unknown upstream.
|
|
989
|
+
provider: observed.provider,
|
|
996
990
|
canonical: observed.canonical,
|
|
997
991
|
attempts: trace.map((row) => row.upstream),
|
|
998
992
|
ms: Date.now() - startedAt,
|
package/lib/catalog.js
CHANGED
|
@@ -167,6 +167,22 @@ function pick(configured, published, fallback) {
|
|
|
167
167
|
return fallback
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Per-request output budget used when nothing else names one.
|
|
172
|
+
*
|
|
173
|
+
* A budget, not a ceiling: the harness materializes it into requests that omit
|
|
174
|
+
* `maxTokens`, and reserves the same number as completion room before it compacts, so
|
|
175
|
+
* this is the number that decides how much of the window stays available to the
|
|
176
|
+
* conversation.
|
|
177
|
+
*
|
|
178
|
+
* 128k doubles the room a long reasoning pass gets without moving that point on a 1M
|
|
179
|
+
* window: the 80% rule stays the binding one until the reserve passes ~144k. It is
|
|
180
|
+
* not applied flat, though — a 128k reserve on a 256k window would leave almost
|
|
181
|
+
* nothing to compact into — so the shipped default scales to the window while a
|
|
182
|
+
* configured value does not, because that one is a choice rather than a default.
|
|
183
|
+
*/
|
|
184
|
+
export const DEFAULT_MAX_TOKENS = 131072
|
|
185
|
+
|
|
170
186
|
/**
|
|
171
187
|
* The seam's whole `ModelModality` vocabulary. A catalog entry may list more
|
|
172
188
|
* than this (models.dev reports `audio` for mimo-v2.5); the seam would reject
|
|
@@ -219,8 +235,11 @@ function resolveOutputBudget(budget, ceiling, contextWindow) {
|
|
|
219
235
|
const most = Number.isFinite(ceiling) && ceiling > 0 ? ceiling : undefined
|
|
220
236
|
const chosen = wanted === undefined ? most : most === undefined ? wanted : Math.min(wanted, most)
|
|
221
237
|
if (chosen === undefined) return undefined
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
if (!Number.isFinite(contextWindow) || contextWindow <= 0) return chosen
|
|
239
|
+
// The shipped default follows the window; a configured value is the user's choice
|
|
240
|
+
// and is only held back from reserving the window outright.
|
|
241
|
+
const share = wanted === DEFAULT_MAX_TOKENS ? 8 : 2
|
|
242
|
+
return Math.min(chosen, Math.floor(contextWindow / share))
|
|
224
243
|
}
|
|
225
244
|
|
|
226
245
|
/**
|
package/lib/client.js
CHANGED
|
@@ -77,6 +77,7 @@ window.__ModuleLoader__.load({
|
|
|
77
77
|
'.cp-input,.cp-select{background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:0.5px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 8px;font-size:13px;min-width:0}',
|
|
78
78
|
'.cp-input:focus-visible,.cp-select:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
|
|
79
79
|
'.cp-btn{border:0.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer}',
|
|
80
|
+
'.cp-btn-on{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}',
|
|
80
81
|
'.cp-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary)}',
|
|
81
82
|
'.cp-btn:disabled{opacity:.5;cursor:default}',
|
|
82
83
|
'.cp-btn-primary{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-base);font-weight:600}',
|
|
@@ -118,6 +119,11 @@ window.__ModuleLoader__.load({
|
|
|
118
119
|
'.cp-acct-newname{flex:0 0 150px}',
|
|
119
120
|
'.cp-acct-add,.cp-acct-key{border-top:0.5px solid var(--dsw-alias-border-l2);padding-top:10px;margin-top:2px}',
|
|
120
121
|
'.cp-chips{display:flex;flex-wrap:wrap;gap:6px}',
|
|
122
|
+
'.cp-chip-manual{border-style:dashed;color:var(--dsw-alias-label-tertiary)}',
|
|
123
|
+
'.cp-small{font-size:11.5px}',
|
|
124
|
+
'.cp-caps{flex-direction:column;gap:8px;display:flex}',
|
|
125
|
+
'.cp-caps-boxes{flex-wrap:wrap;gap:10px}',
|
|
126
|
+
'.cp-cap{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer}',
|
|
121
127
|
'.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
128
|
'.cp-chip-on{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
123
129
|
'.cp-chip-excluded{text-decoration:line-through;color:var(--dsw-alias-label-tertiary)}',
|
|
@@ -194,6 +200,7 @@ window.__ModuleLoader__.load({
|
|
|
194
200
|
'.cp-history-load-row>span{white-space:nowrap}',
|
|
195
201
|
'.cp-history-load-row>span:first-child{flex:none}',
|
|
196
202
|
'.cp-history-load-row>span:last-child{min-width:0;overflow:hidden;text-overflow:ellipsis;text-align:right}',
|
|
203
|
+
'.cp-history-token-stack{display:flex;flex-direction:column;gap:1px;align-items:center}',
|
|
197
204
|
'.cp-history-meta{display:flex;align-items:center;justify-content:center;gap:6px;min-width:0;overflow:hidden}',
|
|
198
205
|
// A failure gets its own row under the entry rather than widening it: the
|
|
199
206
|
// gateway's messages are long enough to wreck a one-line layout.
|
|
@@ -355,13 +362,15 @@ window.__ModuleLoader__.load({
|
|
|
355
362
|
historyUpstream: '上游',
|
|
356
363
|
historyLoad: '延迟',
|
|
357
364
|
historyTokens: 'TOKEN',
|
|
365
|
+
historyCacheHit: '缓存',
|
|
358
366
|
historyFirstChunk: '首字',
|
|
359
367
|
historyTotal: '总耗时',
|
|
360
368
|
historyRate: '输出速率',
|
|
361
369
|
historyTokensHint: '↓输入 ↑输出 ⚡缓存 🧠推理;— 表示网关本次未返回用量',
|
|
362
370
|
historyEffort: '推理强度',
|
|
363
371
|
historyAuto: '(自动)',
|
|
364
|
-
|
|
372
|
+
historyUnknown: '(未上报)',
|
|
373
|
+
noChannels: '未探测到渠道,可手动输入',
|
|
365
374
|
pinned: '已钉住',
|
|
366
375
|
auto: '自动路由',
|
|
367
376
|
modeAuto: '智能路由',
|
|
@@ -374,6 +383,24 @@ window.__ModuleLoader__.load({
|
|
|
374
383
|
reset: '恢复自动',
|
|
375
384
|
refresh: '刷新',
|
|
376
385
|
channels: '渠道(点选=钉住顺序,⊘=排除)',
|
|
386
|
+
channelPlaceholder: '输入渠道名,回车添加',
|
|
387
|
+
budget: '输出预算',
|
|
388
|
+
budgetCurrent: '{n} tok',
|
|
389
|
+
budgetRecommended: '128K(推荐)',
|
|
390
|
+
budgetOfficial: '256K(对齐官方)',
|
|
391
|
+
budgetCustom: '自定义 token 数',
|
|
392
|
+
channelAdd: '添加',
|
|
393
|
+
channelUnverified: '未验证',
|
|
394
|
+
channelsKnown: '已探测:{list}',
|
|
395
|
+
capabilities: '上游能力',
|
|
396
|
+
capabilitiesExtraPlaceholder: '如 quantization:fp8 或 !quantization:bf16',
|
|
397
|
+
capabilitiesDirect: '最近一次探测走 OpenRouter,该路由会忽略此筛选',
|
|
398
|
+
capFree: '免费',
|
|
399
|
+
capImplicitCaching: '隐式缓存',
|
|
400
|
+
capReasoning: '推理',
|
|
401
|
+
capStructuredOutput: '结构化输出',
|
|
402
|
+
capToolUse: '工具调用',
|
|
403
|
+
capVision: '视觉',
|
|
377
404
|
history: '最近请求',
|
|
378
405
|
noHistory: '暂无记录。',
|
|
379
406
|
busy: '处理中…',
|
|
@@ -447,13 +474,15 @@ window.__ModuleLoader__.load({
|
|
|
447
474
|
historyUpstream: 'Upstream',
|
|
448
475
|
historyLoad: 'Latency',
|
|
449
476
|
historyTokens: 'Tokens',
|
|
477
|
+
historyCacheHit: 'cache',
|
|
450
478
|
historyFirstChunk: 'first',
|
|
451
479
|
historyTotal: 'total',
|
|
452
480
|
historyRate: 'rate',
|
|
453
481
|
historyTokensHint: '↓in ↑out ⚡cache 🧠reasoning; a dash means the gateway reported no usage',
|
|
454
482
|
historyEffort: 'effort',
|
|
455
483
|
historyAuto: '(auto)',
|
|
456
|
-
|
|
484
|
+
historyUnknown: '(unreported)',
|
|
485
|
+
noChannels: 'Nothing probed yet — type a name',
|
|
457
486
|
pinned: 'Pinned',
|
|
458
487
|
auto: 'Auto',
|
|
459
488
|
modeAuto: 'Smart routing',
|
|
@@ -466,6 +495,24 @@ window.__ModuleLoader__.load({
|
|
|
466
495
|
reset: 'Back to auto',
|
|
467
496
|
refresh: 'Refresh',
|
|
468
497
|
channels: 'Channels (click = pin order, ⊘ = exclude)',
|
|
498
|
+
channelPlaceholder: 'Channel name, Enter to add',
|
|
499
|
+
budget: 'Output budget',
|
|
500
|
+
budgetCurrent: '{n} tok',
|
|
501
|
+
budgetRecommended: '128K (recommended)',
|
|
502
|
+
budgetOfficial: '256K (matches official)',
|
|
503
|
+
budgetCustom: 'Custom token count',
|
|
504
|
+
channelAdd: 'Add',
|
|
505
|
+
channelUnverified: 'unverified',
|
|
506
|
+
channelsKnown: 'Probed: {list}',
|
|
507
|
+
capabilities: 'Upstream capabilities',
|
|
508
|
+
capabilitiesExtraPlaceholder: 'e.g. quantization:fp8 or !quantization:bf16',
|
|
509
|
+
capabilitiesDirect: 'The last probe saw OpenRouter; that route ignores this filter',
|
|
510
|
+
capFree: 'Free',
|
|
511
|
+
capImplicitCaching: 'Implicit caching',
|
|
512
|
+
capReasoning: 'Reasoning',
|
|
513
|
+
capStructuredOutput: 'Structured output',
|
|
514
|
+
capToolUse: 'Tool use',
|
|
515
|
+
capVision: 'Vision',
|
|
469
516
|
history: 'Recent requests',
|
|
470
517
|
noHistory: 'Nothing recorded.',
|
|
471
518
|
busy: 'Working…',
|
|
@@ -751,6 +798,7 @@ window.__ModuleLoader__.load({
|
|
|
751
798
|
pinModel: (payload) => controller.pinModel(payload),
|
|
752
799
|
setModelVisible: (model, visible) => controller.setModelVisible(model, visible),
|
|
753
800
|
setToolsExposed: (value) => controller.setToolsExposed(value),
|
|
801
|
+
setBudget: (value) => controller.setBudget(value),
|
|
754
802
|
setModelsVisibility: (mode) => controller.setModelsVisibility(mode),
|
|
755
803
|
probeModel: (model) => controller.probeModel(model),
|
|
756
804
|
validateModel: (model) => controller.validateModel(model),
|
|
@@ -941,6 +989,13 @@ window.__ModuleLoader__.load({
|
|
|
941
989
|
return result
|
|
942
990
|
}
|
|
943
991
|
|
|
992
|
+
/** Set the per-request output budget, in tokens. */
|
|
993
|
+
async setBudget(value) {
|
|
994
|
+
const result = await this.run('budget', async () => await this.call('budget.set', { value }))
|
|
995
|
+
if (result !== undefined) this.applyState(result)
|
|
996
|
+
return result
|
|
997
|
+
}
|
|
998
|
+
|
|
944
999
|
/**
|
|
945
1000
|
* Show, hide or swap every subscription model at once.
|
|
946
1001
|
*
|
|
@@ -1123,47 +1178,188 @@ window.__ModuleLoader__.load({
|
|
|
1123
1178
|
}
|
|
1124
1179
|
|
|
1125
1180
|
/** The channel chooser for one model. */
|
|
1126
|
-
|
|
1181
|
+
/** The capability names the gateway accepts, in its own spelling. */
|
|
1182
|
+
const CAPABILITIES = [
|
|
1183
|
+
['free', 'capFree'],
|
|
1184
|
+
['implicit-caching', 'capImplicitCaching'],
|
|
1185
|
+
['reasoning', 'capReasoning'],
|
|
1186
|
+
['structured-output', 'capStructuredOutput'],
|
|
1187
|
+
['tool-use', 'capToolUse'],
|
|
1188
|
+
['vision', 'capVision'],
|
|
1189
|
+
]
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* The channel list, editable.
|
|
1193
|
+
*
|
|
1194
|
+
* The chips used to be exactly what a probe returned, which made an account whose
|
|
1195
|
+
* probe came back empty a dead end: the row said no channels were probed and
|
|
1196
|
+
* offered no way to name the upstream the user already knew served them. The list
|
|
1197
|
+
* is the user's data now — a probe fills it in, typing fills it in, and both are
|
|
1198
|
+
* the same list.
|
|
1199
|
+
*/
|
|
1200
|
+
function ChannelEditor(props) {
|
|
1127
1201
|
const { t, model, disabled, onPin } = props
|
|
1128
|
-
|
|
1202
|
+
const [draft, setDraft] = React.useState('')
|
|
1129
1203
|
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
|
-
|
|
1204
|
+
const known = model.upstreams
|
|
1205
|
+
// A chosen name shows even when no probe reported it: a chip that disappears is
|
|
1206
|
+
// a chip that cannot be removed.
|
|
1207
|
+
const names = [...new Set([...known, ...model.pinned, ...model.excluded])]
|
|
1208
|
+
const pin = (next) => {
|
|
1209
|
+
if (disabled) return
|
|
1210
|
+
onPin({ model: model.id, upstreams: next })
|
|
1211
|
+
}
|
|
1212
|
+
const add = () => {
|
|
1213
|
+
const name = draft.trim()
|
|
1214
|
+
if (name === '') return
|
|
1215
|
+
setDraft('')
|
|
1216
|
+
const needle = name.toLowerCase()
|
|
1217
|
+
if (model.pinned.some((entry) => String(entry).toLowerCase() === needle)) return
|
|
1218
|
+
pin([...model.pinned, name])
|
|
1219
|
+
}
|
|
1220
|
+
return h('div', { className: 'cp-caps' }, [
|
|
1221
|
+
h('div', { key: 'row', className: 'cp-row' }, [
|
|
1222
|
+
h('span', { key: 'label', className: 'cp-muted' }, t('channels')),
|
|
1223
|
+
names.length === 0
|
|
1224
|
+
? h('span', { key: 'none', className: 'cp-muted' }, t('noChannels'))
|
|
1225
|
+
: h('span', { key: 'chips', className: 'cp-chips' }, names.map((upstream) => {
|
|
1226
|
+
const pinned = model.pinned.includes(upstream)
|
|
1227
|
+
const excluded = model.excluded.includes(upstream)
|
|
1228
|
+
const probed = known.includes(upstream)
|
|
1229
|
+
const verdict = verdicts.get(upstream)
|
|
1230
|
+
const detail = verdict === undefined
|
|
1231
|
+
? ''
|
|
1232
|
+
: ` · ${VERDICT[verdict.status] ?? verdict.status}${verdict.note ? `\n${verdict.note}` : ''}`
|
|
1233
|
+
return h('span', {
|
|
1234
|
+
key: upstream,
|
|
1235
|
+
className: `cp-chip${pinned ? ' cp-chip-on' : ''}${excluded ? ' cp-chip-excluded' : ''}${probed ? '' : ' cp-chip-manual'}`,
|
|
1236
|
+
}, [
|
|
1237
|
+
h('span', {
|
|
1238
|
+
key: 'pin',
|
|
1239
|
+
role: 'button',
|
|
1240
|
+
tabIndex: 0,
|
|
1241
|
+
title: `${upstream}${probed ? '' : ` · ${t('channelUnverified')}`}${detail}`,
|
|
1242
|
+
onClick: () => {
|
|
1243
|
+
if (disabled) return
|
|
1244
|
+
const next = pinned ? model.pinned.filter((name) => name !== upstream) : [...model.pinned, upstream]
|
|
1245
|
+
onPin({ model: model.id, upstreams: next })
|
|
1246
|
+
},
|
|
1247
|
+
}, [
|
|
1248
|
+
h('span', { key: 'dot', style: { marginRight: 5 } }, h(Dot, { color: verdictColor(verdict?.status ?? 'unknown'), title: VERDICT[verdict?.status ?? 'unknown'] })),
|
|
1249
|
+
upstream,
|
|
1250
|
+
]),
|
|
1251
|
+
h('span', {
|
|
1252
|
+
key: 'ex',
|
|
1253
|
+
role: 'button',
|
|
1254
|
+
tabIndex: 0,
|
|
1255
|
+
title: excluded ? 'un-exclude' : 'exclude',
|
|
1256
|
+
onClick: () => {
|
|
1257
|
+
if (disabled) return
|
|
1258
|
+
const next = excluded ? model.excluded.filter((name) => name !== upstream) : [...model.excluded, upstream]
|
|
1259
|
+
onPin({ model: model.id, exclude: next })
|
|
1260
|
+
},
|
|
1261
|
+
}, '⊘'),
|
|
1262
|
+
// A name that only exists because someone typed it has no discovery
|
|
1263
|
+
// list to fall back on, so it needs an explicit way out: un-pinning
|
|
1264
|
+
// removes it, and this is what says so.
|
|
1265
|
+
probed ? null : h('span', {
|
|
1266
|
+
key: 'del',
|
|
1267
|
+
role: 'button',
|
|
1268
|
+
tabIndex: 0,
|
|
1269
|
+
title: 'remove',
|
|
1270
|
+
onClick: () => {
|
|
1271
|
+
if (disabled) return
|
|
1272
|
+
onPin({
|
|
1273
|
+
model: model.id,
|
|
1274
|
+
upstreams: model.pinned.filter((name) => name !== upstream),
|
|
1275
|
+
exclude: model.excluded.filter((name) => name !== upstream),
|
|
1276
|
+
})
|
|
1277
|
+
},
|
|
1278
|
+
}, '×'),
|
|
1279
|
+
])
|
|
1280
|
+
})),
|
|
1281
|
+
]),
|
|
1282
|
+
h('div', { key: 'add', className: 'cp-row' }, [
|
|
1283
|
+
h('input', {
|
|
1284
|
+
key: 'in',
|
|
1285
|
+
type: 'text',
|
|
1286
|
+
className: 'cp-input cp-grow',
|
|
1287
|
+
value: draft,
|
|
1288
|
+
disabled,
|
|
1289
|
+
placeholder: t('channelPlaceholder'),
|
|
1290
|
+
'aria-label': `${t('channelAdd')}: ${model.id}`,
|
|
1291
|
+
onChange: (event) => setDraft(event.target.value),
|
|
1292
|
+
onKeyDown: (event) => {
|
|
1293
|
+
if (event.key !== 'Enter') return
|
|
1294
|
+
event.preventDefault()
|
|
1295
|
+
add()
|
|
1296
|
+
},
|
|
1297
|
+
}),
|
|
1298
|
+
h('button', { key: 'b', type: 'button', className: 'cp-btn', disabled, onClick: add }, t('channelAdd')),
|
|
1299
|
+
]),
|
|
1300
|
+
known.length === 0 ? null : h('div', { key: 'known', className: 'cp-muted cp-small' }, fill(t('channelsKnown'), { list: known.join(' · ') })),
|
|
1301
|
+
])
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* The capabilities an upstream must offer for this model.
|
|
1306
|
+
*
|
|
1307
|
+
* The gateway applies this itself and rejects a name it does not know, so the six
|
|
1308
|
+
* it documents are checkboxes and anything else — a weight format such as
|
|
1309
|
+
* `quantization:fp8`, or its negation — is typed. It only exists on the planner
|
|
1310
|
+
* pipeline: OpenRouter has no equivalent, and a model known to run there says so
|
|
1311
|
+
* rather than letting a filter look active while it is ignored.
|
|
1312
|
+
*/
|
|
1313
|
+
function CapabilityFilter(props) {
|
|
1314
|
+
const { t, model, disabled, onPin } = props
|
|
1315
|
+
const [extra, setExtra] = React.useState('')
|
|
1316
|
+
const chosen = model.has ?? []
|
|
1317
|
+
const write = (next) => {
|
|
1318
|
+
if (disabled) return
|
|
1319
|
+
onPin({ model: model.id, has: next })
|
|
1320
|
+
}
|
|
1321
|
+
const toggle = (id) => write(chosen.includes(id) ? chosen.filter((name) => name !== id) : [...chosen, id])
|
|
1322
|
+
const addExtra = () => {
|
|
1323
|
+
const value = extra.trim()
|
|
1324
|
+
if (value === '') return
|
|
1325
|
+
setExtra('')
|
|
1326
|
+
if (chosen.includes(value)) return
|
|
1327
|
+
write([...chosen, value])
|
|
1328
|
+
}
|
|
1329
|
+
const extras = chosen.filter((name) => !CAPABILITIES.some(([id]) => id === name))
|
|
1330
|
+
return h('div', { className: 'cp-caps' }, [
|
|
1331
|
+
h('div', { key: 'head', className: 'cp-row' }, [
|
|
1332
|
+
h('span', { key: 'l', className: 'cp-muted' }, t('capabilities')),
|
|
1333
|
+
model.pipeline === 'direct' ? h('span', { key: 'w', className: 'cp-muted cp-small' }, t('capabilitiesDirect')) : null,
|
|
1334
|
+
]),
|
|
1335
|
+
h('span', { key: 'boxes', className: 'cp-row cp-caps-boxes' }, CAPABILITIES.map(([id, key]) =>
|
|
1336
|
+
h('label', { key: id, className: 'cp-cap' }, [
|
|
1337
|
+
h('input', {
|
|
1338
|
+
key: 'c', type: 'checkbox', className: 'cp-check',
|
|
1339
|
+
checked: chosen.includes(id), disabled, onChange: () => toggle(id),
|
|
1340
|
+
}),
|
|
1341
|
+
h('span', { key: 't' }, t(key)),
|
|
1342
|
+
]))),
|
|
1343
|
+
h('div', { key: 'extra', className: 'cp-row' }, [
|
|
1344
|
+
h('input', {
|
|
1345
|
+
key: 'in', type: 'text', className: 'cp-input cp-grow',
|
|
1346
|
+
value: extra, disabled,
|
|
1347
|
+
placeholder: t('capabilitiesExtraPlaceholder'),
|
|
1348
|
+
'aria-label': `${t('capabilities')}: ${model.id}`,
|
|
1349
|
+
onChange: (event) => setExtra(event.target.value),
|
|
1350
|
+
onKeyDown: (event) => {
|
|
1351
|
+
if (event.key !== 'Enter') return
|
|
1352
|
+
event.preventDefault()
|
|
1353
|
+
addExtra()
|
|
1354
|
+
},
|
|
1355
|
+
}),
|
|
1356
|
+
h('button', { key: 'b', type: 'button', className: 'cp-btn', disabled, onClick: addExtra }, t('channelAdd')),
|
|
1357
|
+
]),
|
|
1358
|
+
extras.length === 0 ? null : h('span', { key: 'list', className: 'cp-chips' }, extras.map((name) =>
|
|
1359
|
+
h('span', { key: name, className: 'cp-chip cp-chip-on' }, [
|
|
1360
|
+
h('span', { key: 'n' }, name),
|
|
1361
|
+
h('span', { key: 'x', role: 'button', tabIndex: 0, title: 'remove', onClick: () => toggle(name) }, '×'),
|
|
1362
|
+
]))),
|
|
1167
1363
|
])
|
|
1168
1364
|
}
|
|
1169
1365
|
|
|
@@ -1230,9 +1426,8 @@ window.__ModuleLoader__.load({
|
|
|
1230
1426
|
action('test', t('testPin'), actions.testModel),
|
|
1231
1427
|
action('reset', t('reset'), actions.resetModel),
|
|
1232
1428
|
]),
|
|
1233
|
-
model.
|
|
1234
|
-
|
|
1235
|
-
: h('div', { key: 'none', className: 'cp-muted' }, t('noChannels')),
|
|
1429
|
+
h(ChannelEditor, { key: 'channels', t, model, disabled, onPin: (payload) => actions.pinModel(payload) }),
|
|
1430
|
+
h(CapabilityFilter, { key: 'caps', t, model, disabled, onPin: (payload) => actions.pinModel(payload) }),
|
|
1236
1431
|
]) : null,
|
|
1237
1432
|
])
|
|
1238
1433
|
}
|
|
@@ -1428,6 +1623,60 @@ window.__ModuleLoader__.load({
|
|
|
1428
1623
|
])
|
|
1429
1624
|
}
|
|
1430
1625
|
|
|
1626
|
+
/**
|
|
1627
|
+
* The per-request output budget.
|
|
1628
|
+
*
|
|
1629
|
+
* One number for the whole route: the harness reserves it as completion room on
|
|
1630
|
+
* every request, so it decides where compaction fires as much as how long one
|
|
1631
|
+
* response may be. The two presets are the shipped default and the figure
|
|
1632
|
+
* DeepSeek's own provider uses; anything else is typed.
|
|
1633
|
+
*/
|
|
1634
|
+
function BudgetCard(props) {
|
|
1635
|
+
const { t, data, disabled, actions } = props
|
|
1636
|
+
const current = Number(data.defaultMaxTokens ?? 0)
|
|
1637
|
+
const [draft, setDraft] = React.useState('')
|
|
1638
|
+
const write = (value) => {
|
|
1639
|
+
if (disabled) return
|
|
1640
|
+
actions.setBudget(value)
|
|
1641
|
+
}
|
|
1642
|
+
const custom = () => {
|
|
1643
|
+
const value = Number(draft.trim())
|
|
1644
|
+
if (!Number.isSafeInteger(value) || value < 1024) return
|
|
1645
|
+
setDraft('')
|
|
1646
|
+
write(value)
|
|
1647
|
+
}
|
|
1648
|
+
return h('div', { className: 'cp-card' }, [
|
|
1649
|
+
h('div', { key: 'title', className: 'cp-card-title' }, `${t('budget')} · ${fill(t('budgetCurrent'), { n: current.toLocaleString('en-US') })}`),
|
|
1650
|
+
h('div', { key: 'row', className: 'cp-row' }, [
|
|
1651
|
+
h('button', {
|
|
1652
|
+
key: 'recommended', type: 'button',
|
|
1653
|
+
className: `cp-btn${current === 131072 ? ' cp-btn-on' : ''}`,
|
|
1654
|
+
disabled, onClick: () => write(131072),
|
|
1655
|
+
}, t('budgetRecommended')),
|
|
1656
|
+
h('button', {
|
|
1657
|
+
key: 'official', type: 'button',
|
|
1658
|
+
className: `cp-btn${current === 256000 ? ' cp-btn-on' : ''}`,
|
|
1659
|
+
disabled, onClick: () => write(256000),
|
|
1660
|
+
}, t('budgetOfficial')),
|
|
1661
|
+
]),
|
|
1662
|
+
h('div', { key: 'custom', className: 'cp-row' }, [
|
|
1663
|
+
h('input', {
|
|
1664
|
+
key: 'in', type: 'text', className: 'cp-input cp-grow',
|
|
1665
|
+
value: draft, disabled,
|
|
1666
|
+
placeholder: t('budgetCustom'),
|
|
1667
|
+
'aria-label': t('budget'),
|
|
1668
|
+
onChange: (event) => setDraft(event.target.value),
|
|
1669
|
+
onKeyDown: (event) => {
|
|
1670
|
+
if (event.key !== 'Enter') return
|
|
1671
|
+
event.preventDefault()
|
|
1672
|
+
custom()
|
|
1673
|
+
},
|
|
1674
|
+
}),
|
|
1675
|
+
h('button', { key: 'b', type: 'button', className: 'cp-btn', disabled, onClick: custom }, t('channelAdd')),
|
|
1676
|
+
]),
|
|
1677
|
+
])
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1431
1680
|
/**
|
|
1432
1681
|
* The models card: a fold-away section, like the request history.
|
|
1433
1682
|
*
|
|
@@ -1487,12 +1736,13 @@ window.__ModuleLoader__.load({
|
|
|
1487
1736
|
*/
|
|
1488
1737
|
function historyRows(t, entry, showAccount) {
|
|
1489
1738
|
const failed = String(entry.error ?? '') !== ''
|
|
1490
|
-
const upstream = `${entry.provider || t('
|
|
1739
|
+
const upstream = `${entry.provider || t('historyUnknown')}${showAccount && entry.account !== '' ? ` · ${entry.account}` : ''}`
|
|
1491
1740
|
// The route prefix is constant across every row of this list, so it is
|
|
1492
1741
|
// carried in the tooltip instead of eating the width the model name needs
|
|
1493
1742
|
// — centred text that overflows loses BOTH ends, not just the tail.
|
|
1494
1743
|
const label = String(entry.model).replace(/^cline-pass\//, '')
|
|
1495
1744
|
const figures = latency(entry)
|
|
1745
|
+
const cacheHit = entry.cacheHit === undefined ? null : entry.cacheHit
|
|
1496
1746
|
const row = h('tr', {
|
|
1497
1747
|
key: 'row',
|
|
1498
1748
|
// The whole row restates itself on hover, including the figures the three
|
|
@@ -1516,7 +1766,7 @@ window.__ModuleLoader__.load({
|
|
|
1516
1766
|
h('td', { key: 'model', className: 'cp-history-model-cell' }, h('div', { className: 'cp-history-model-stack' }, [
|
|
1517
1767
|
h('span', { key: 'name', className: 'cp-history-model', title: entry.model }, label),
|
|
1518
1768
|
h('span', { key: 'upstream', className: 'cp-history-meta' }, [
|
|
1519
|
-
h('span', { key: 'provider', className: 'cp-tag' }, entry.provider || t('
|
|
1769
|
+
h('span', { key: 'provider', className: 'cp-tag' }, entry.provider || t('historyUnknown')),
|
|
1520
1770
|
showAccount && entry.account !== '' ? h('span', { key: 'account', className: 'cp-tag cp-tag-quiet' }, entry.account) : null,
|
|
1521
1771
|
]),
|
|
1522
1772
|
])),
|
|
@@ -1526,8 +1776,11 @@ window.__ModuleLoader__.load({
|
|
|
1526
1776
|
h('td', {
|
|
1527
1777
|
key: 'tokens',
|
|
1528
1778
|
className: 'cp-history-tokens',
|
|
1529
|
-
title: `${tokenSummaryExact(entry)}\n${t('historyTokensHint')}`,
|
|
1530
|
-
},
|
|
1779
|
+
title: `${tokenSummaryExact(entry)}\n${cacheHit === null ? '' : `${t('historyCacheHit')} ${cacheHit.toFixed(2)}%\n`}${t('historyTokensHint')}`,
|
|
1780
|
+
}, h('div', { className: 'cp-history-token-stack' }, [
|
|
1781
|
+
h('span', { key: 'sum' }, tokenSummary(entry)),
|
|
1782
|
+
cacheHit === null ? null : h('span', { key: 'cache', className: 'cp-muted cp-small' }, `${t('historyCacheHit')} ${cacheHit.toFixed(2)}%`),
|
|
1783
|
+
])),
|
|
1531
1784
|
h('td', { key: 'load', className: 'cp-history-load' }, [
|
|
1532
1785
|
h('span', { key: 'first', className: 'cp-history-load-row' }, [
|
|
1533
1786
|
h('span', { key: 'k', className: 'cp-muted' }, t('historyFirstChunk')),
|
|
@@ -1780,6 +2033,7 @@ window.__ModuleLoader__.load({
|
|
|
1780
2033
|
pinModel: props.pinModel,
|
|
1781
2034
|
setModelVisible: props.setModelVisible,
|
|
1782
2035
|
setToolsExposed: props.setToolsExposed,
|
|
2036
|
+
setBudget: props.setBudget,
|
|
1783
2037
|
setModelsVisibility: props.setModelsVisibility,
|
|
1784
2038
|
probeModel: props.probeModel,
|
|
1785
2039
|
validateModel: props.validateModel,
|
|
@@ -1837,8 +2091,9 @@ window.__ModuleLoader__.load({
|
|
|
1837
2091
|
// Everything below the accounts card reads through the key that card
|
|
1838
2092
|
// stores, so none of it exists until there is one.
|
|
1839
2093
|
data === null || !ready ? null : h(ModelsCard, { key: 'models', t, data, disabled, busy: state.busy, actions }),
|
|
1840
|
-
// Route-wide
|
|
2094
|
+
// Route-wide settings last: they are not about one model or account, and
|
|
1841
2095
|
// nothing below them depends on their value.
|
|
2096
|
+
data === null ? null : h(BudgetCard, { key: 'budget', t, data, disabled: state.busy !== null || data.settingsAvailable === false, actions }),
|
|
1842
2097
|
data === null ? null : h('div', { key: 'tools', className: 'cp-card' }, [
|
|
1843
2098
|
h('label', { key: 'head', className: 'cp-row cp-tools-label' }, [
|
|
1844
2099
|
h('input', {
|
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
|
/**
|
|
@@ -149,6 +160,11 @@ export const Config = z.object({
|
|
|
149
160
|
* larger, and reporting that here reserves it on every request — at the shipped
|
|
150
161
|
* 384k figure a 1M window compacts at 55% instead of the 80% the threshold ratio
|
|
151
162
|
* asks for. Each model's published ceiling clamps whatever is set here.
|
|
163
|
+
*
|
|
164
|
+
* Nothing here may reserve more than half the window. The shipped default goes
|
|
165
|
+
* further and scales to an eighth on a small window, because a flat 128k reserve
|
|
166
|
+
* leaves a 256k model almost nothing to compact into; an explicit value counts as a
|
|
167
|
+
* choice and only gets the half-window limit.
|
|
152
168
|
*/
|
|
153
169
|
defaultMaxTokens: z.number().default(DEFAULT_MAX_TOKENS),
|
|
154
170
|
streamIdleTimeoutMs: z.number().default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
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 ?? ''),
|
|
@@ -129,6 +130,9 @@ export function createPanel({ control, engine, store }) {
|
|
|
129
130
|
provider: control.providerName,
|
|
130
131
|
displayName: control.displayName,
|
|
131
132
|
baseURL: String(config.baseURL ?? ''),
|
|
133
|
+
// The per-request output budget: one number for the whole route, because it
|
|
134
|
+
// decides how much of every window the harness holds back as completion room.
|
|
135
|
+
defaultMaxTokens: Number(config.defaultMaxTokens ?? 0),
|
|
132
136
|
settingsAvailable: control.settingsAvailable(),
|
|
133
137
|
accountMode: config.accountMode === 'roundrobin' ? 'roundrobin' : 'single',
|
|
134
138
|
activeAccount: String(config.activeAccount ?? ''),
|
|
@@ -377,6 +381,9 @@ export function createPanel({ control, engine, store }) {
|
|
|
377
381
|
sort: payload?.sort === undefined
|
|
378
382
|
? (current.sort ?? '')
|
|
379
383
|
: (payload.sort === 'none' || payload.sort === '' ? '' : String(payload.sort)),
|
|
384
|
+
has: Array.isArray(payload?.has)
|
|
385
|
+
? [...new Set(payload.has.map((name) => String(name).trim()).filter((name) => name.length > 0))]
|
|
386
|
+
: (current.has ?? []).map(String),
|
|
380
387
|
}
|
|
381
388
|
await control.updateConfig({ perModel: { ...(config.perModel ?? {}), [model]: next } })
|
|
382
389
|
return { model, pin: next, ...(await state()) }
|
|
@@ -445,6 +452,21 @@ export function createPanel({ control, engine, store }) {
|
|
|
445
452
|
return await modelPin({ model: modelOf(payload), upstreams: [], exclude: [], sort: 'none', pinMode: 'preferred' })
|
|
446
453
|
}
|
|
447
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Cache reads as a share of billed prompt tokens, or null when nothing was measured.
|
|
457
|
+
*
|
|
458
|
+
* A ratio over zero is not a reading, and 0% would claim "nothing cached" for a
|
|
459
|
+
* request the gateway never reported usage for.
|
|
460
|
+
*/
|
|
461
|
+
function cacheHitOf(usage) {
|
|
462
|
+
if (usage === null || usage === undefined) return null
|
|
463
|
+
const cached = Number(usage.cacheReadTokens ?? 0)
|
|
464
|
+
const billed = cached + Number(usage.inputTokens ?? 0)
|
|
465
|
+
// Two decimals: an integer round turns 99.6% into a flat 100%, which reads as a
|
|
466
|
+
// claim the numbers do not support.
|
|
467
|
+
return billed <= 0 ? null : Math.round((cached / billed) * 10000) / 100
|
|
468
|
+
}
|
|
469
|
+
|
|
448
470
|
/** The live request history, newest first. */
|
|
449
471
|
async function history(payload) {
|
|
450
472
|
const limit = Number.isSafeInteger(payload?.limit) && payload.limit > 0 ? Math.min(payload.limit, 100) : 25
|
|
@@ -469,6 +491,7 @@ export function createPanel({ control, engine, store }) {
|
|
|
469
491
|
cacheReadTokens: Number(entry.usage?.cacheReadTokens ?? 0),
|
|
470
492
|
reasoningTokens: Number(entry.usage?.reasoningTokens ?? 0),
|
|
471
493
|
},
|
|
494
|
+
cacheHit: cacheHitOf(entry.usage),
|
|
472
495
|
effort: String(entry.effort ?? ''),
|
|
473
496
|
stream: entry.stream === true,
|
|
474
497
|
error: firstLine(entry.error, 200),
|
|
@@ -486,6 +509,23 @@ export function createPanel({ control, engine, store }) {
|
|
|
486
509
|
* The response carries `restartRequired` so the panel can say so instead of
|
|
487
510
|
* implying the switch already took effect.
|
|
488
511
|
*/
|
|
512
|
+
/**
|
|
513
|
+
* Set the per-request output budget.
|
|
514
|
+
*
|
|
515
|
+
* The panel offers the two numbers that matter — the shipped default, and the one
|
|
516
|
+
* DeepSeek's own provider uses — plus whatever a user types. It is a route-wide
|
|
517
|
+
* setting rather than a per-model one because it is the same policy for every
|
|
518
|
+
* model: how much of a window to hold back for one response.
|
|
519
|
+
*/
|
|
520
|
+
async function budgetSet(payload) {
|
|
521
|
+
const value = Number(payload?.value)
|
|
522
|
+
if (!Number.isSafeInteger(value) || value < 1024 || value > 2000000) {
|
|
523
|
+
throw new Error(`the output budget must be a whole number of tokens between 1024 and 2000000 (got ${JSON.stringify(payload?.value)})`)
|
|
524
|
+
}
|
|
525
|
+
await control.updateConfig({ defaultMaxTokens: value })
|
|
526
|
+
return await state()
|
|
527
|
+
}
|
|
528
|
+
|
|
489
529
|
async function toolsExpose(payload) {
|
|
490
530
|
if (typeof payload?.value !== 'boolean') throw new Error('this action needs a boolean `value`')
|
|
491
531
|
await control.updateConfig({ exposeTools: payload.value })
|
|
@@ -510,6 +550,7 @@ export function createPanel({ control, engine, store }) {
|
|
|
510
550
|
'model.reset': modelReset,
|
|
511
551
|
'models.refresh': modelsRefresh,
|
|
512
552
|
'tools.expose': toolsExpose,
|
|
553
|
+
'budget.set': budgetSet,
|
|
513
554
|
history,
|
|
514
555
|
}
|
|
515
556
|
}
|
package/lib/protocol.js
CHANGED
|
@@ -59,7 +59,15 @@ export function isAuthenticationError(message, status) {
|
|
|
59
59
|
export function parseRouting(json) {
|
|
60
60
|
const payload = unwrapEnvelope(json)
|
|
61
61
|
const message = payload?.choices?.[0]?.message
|
|
62
|
-
|
|
62
|
+
// A non-streaming answer carries routing on the message; a stream carries the same
|
|
63
|
+
// object on the delta instead. Reading only the message left every streamed request
|
|
64
|
+
// with no upstream at all, which the history then filled in with the pinned name —
|
|
65
|
+
// reporting a pin the gateway may never have adopted.
|
|
66
|
+
const delta = payload?.choices?.[0]?.delta
|
|
67
|
+
const routing = message?.provider_metadata?.gateway?.routing
|
|
68
|
+
?? delta?.provider_metadata?.gateway?.routing
|
|
69
|
+
?? payload?.provider_metadata?.gateway?.routing
|
|
70
|
+
?? {}
|
|
63
71
|
const direct = typeof payload?.provider === 'string' ? payload.provider : null
|
|
64
72
|
return {
|
|
65
73
|
content: typeof message?.content === 'string' ? message.content : null,
|
|
@@ -184,6 +192,9 @@ function knownChannels(meta, pipeline) {
|
|
|
184
192
|
export function injectPrefs(body, meta, attempt) {
|
|
185
193
|
const next = { ...body }
|
|
186
194
|
const { upstream = null, orderRest = [], excludeList = [], strict = true } = attempt ?? {}
|
|
195
|
+
// `has` names the capabilities an upstream must offer. There is no equivalent on
|
|
196
|
+
// the OpenRouter spelling, so a direct-pipeline model simply does not carry it.
|
|
197
|
+
const required = (Array.isArray(attempt?.has) ? attempt.has : []).map((name) => String(name).trim()).filter((name) => name.length > 0)
|
|
187
198
|
// Normalized here as well as in buildAttempts: this function is exported, so
|
|
188
199
|
// a caller can hand it an attempt straight from configuration.
|
|
189
200
|
const sort = normalizeSort(attempt?.sort)
|
|
@@ -197,7 +208,7 @@ export function injectPrefs(body, meta, attempt) {
|
|
|
197
208
|
const both = attempt?.spelling !== 'detected'
|
|
198
209
|
const useGateway = both || pipeline === 'planner' || pipeline === null
|
|
199
210
|
const useOpenRouter = both || pipeline === 'direct' || pipeline === null
|
|
200
|
-
const gatewayUsable = useGateway && (upstream !== null || sort !== null || (gatewayAllow !== null && gatewayAllow.length > 0))
|
|
211
|
+
const gatewayUsable = useGateway && (upstream !== null || sort !== null || required.length > 0 || (gatewayAllow !== null && gatewayAllow.length > 0))
|
|
201
212
|
// `ignore` is OpenRouter's own exclusion field: written alongside the
|
|
202
213
|
// allow-list so an exclusion still bites when the allow-list cannot be
|
|
203
214
|
// computed (no channel list yet) or has gone stale.
|
|
@@ -215,6 +226,7 @@ export function injectPrefs(body, meta, attempt) {
|
|
|
215
226
|
gateway.only = spell(gatewayAllow, 'planner')
|
|
216
227
|
}
|
|
217
228
|
if (sort !== null) gateway.sort = sort
|
|
229
|
+
if (required.length > 0) gateway.has = required
|
|
218
230
|
next.providerOptions = { ...(next.providerOptions ?? {}), gateway: { ...(next.providerOptions?.gateway ?? {}), ...gateway } }
|
|
219
231
|
}
|
|
220
232
|
if (openRouterUsable) {
|
|
@@ -316,7 +328,12 @@ export function buildAttempts(config) {
|
|
|
316
328
|
const wanted = listed.filter((name) => !excluded.has(normalizeSlug(name)))
|
|
317
329
|
const strict = normalizePinMode(config?.pinMode) === 'strict'
|
|
318
330
|
const sort = normalizeSort(config?.sort)
|
|
319
|
-
|
|
331
|
+
// The capability filter is gateway-only and applies to every attempt, so it rides
|
|
332
|
+
// in the shared base rather than being threaded through each candidate.
|
|
333
|
+
const has = (Array.isArray(config?.has) ? config.has : [])
|
|
334
|
+
.map((name) => String(name).trim())
|
|
335
|
+
.filter((name) => name.length > 0)
|
|
336
|
+
const base = { strict, sort, excludeList: exclude, ...(has.length > 0 ? { has } : {}) }
|
|
320
337
|
// Carried through only when the caller configured it, so an attempt built from
|
|
321
338
|
// a bare pin profile keeps its historical shape.
|
|
322
339
|
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.12",
|
|
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",
|