dsh-cline-pass 0.2.11 → 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 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
  *
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
- const half = Number.isFinite(contextWindow) && contextWindow > 0 ? Math.floor(contextWindow / 2) : undefined
223
- return half === undefined ? chosen : Math.min(chosen, half)
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}',
@@ -199,6 +200,7 @@ window.__ModuleLoader__.load({
199
200
  '.cp-history-load-row>span{white-space:nowrap}',
200
201
  '.cp-history-load-row>span:first-child{flex:none}',
201
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}',
202
204
  '.cp-history-meta{display:flex;align-items:center;justify-content:center;gap:6px;min-width:0;overflow:hidden}',
203
205
  // A failure gets its own row under the entry rather than widening it: the
204
206
  // gateway's messages are long enough to wreck a one-line layout.
@@ -360,6 +362,7 @@ window.__ModuleLoader__.load({
360
362
  historyUpstream: '上游',
361
363
  historyLoad: '延迟',
362
364
  historyTokens: 'TOKEN',
365
+ historyCacheHit: '缓存',
363
366
  historyFirstChunk: '首字',
364
367
  historyTotal: '总耗时',
365
368
  historyRate: '输出速率',
@@ -381,6 +384,11 @@ window.__ModuleLoader__.load({
381
384
  refresh: '刷新',
382
385
  channels: '渠道(点选=钉住顺序,⊘=排除)',
383
386
  channelPlaceholder: '输入渠道名,回车添加',
387
+ budget: '输出预算',
388
+ budgetCurrent: '{n} tok',
389
+ budgetRecommended: '128K(推荐)',
390
+ budgetOfficial: '256K(对齐官方)',
391
+ budgetCustom: '自定义 token 数',
384
392
  channelAdd: '添加',
385
393
  channelUnverified: '未验证',
386
394
  channelsKnown: '已探测:{list}',
@@ -466,6 +474,7 @@ window.__ModuleLoader__.load({
466
474
  historyUpstream: 'Upstream',
467
475
  historyLoad: 'Latency',
468
476
  historyTokens: 'Tokens',
477
+ historyCacheHit: 'cache',
469
478
  historyFirstChunk: 'first',
470
479
  historyTotal: 'total',
471
480
  historyRate: 'rate',
@@ -487,6 +496,11 @@ window.__ModuleLoader__.load({
487
496
  refresh: 'Refresh',
488
497
  channels: 'Channels (click = pin order, ⊘ = exclude)',
489
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',
490
504
  channelAdd: 'Add',
491
505
  channelUnverified: 'unverified',
492
506
  channelsKnown: 'Probed: {list}',
@@ -784,6 +798,7 @@ window.__ModuleLoader__.load({
784
798
  pinModel: (payload) => controller.pinModel(payload),
785
799
  setModelVisible: (model, visible) => controller.setModelVisible(model, visible),
786
800
  setToolsExposed: (value) => controller.setToolsExposed(value),
801
+ setBudget: (value) => controller.setBudget(value),
787
802
  setModelsVisibility: (mode) => controller.setModelsVisibility(mode),
788
803
  probeModel: (model) => controller.probeModel(model),
789
804
  validateModel: (model) => controller.validateModel(model),
@@ -974,6 +989,13 @@ window.__ModuleLoader__.load({
974
989
  return result
975
990
  }
976
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
+
977
999
  /**
978
1000
  * Show, hide or swap every subscription model at once.
979
1001
  *
@@ -1601,6 +1623,60 @@ window.__ModuleLoader__.load({
1601
1623
  ])
1602
1624
  }
1603
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
+
1604
1680
  /**
1605
1681
  * The models card: a fold-away section, like the request history.
1606
1682
  *
@@ -1666,6 +1742,7 @@ window.__ModuleLoader__.load({
1666
1742
  // — centred text that overflows loses BOTH ends, not just the tail.
1667
1743
  const label = String(entry.model).replace(/^cline-pass\//, '')
1668
1744
  const figures = latency(entry)
1745
+ const cacheHit = entry.cacheHit === undefined ? null : entry.cacheHit
1669
1746
  const row = h('tr', {
1670
1747
  key: 'row',
1671
1748
  // The whole row restates itself on hover, including the figures the three
@@ -1699,8 +1776,11 @@ window.__ModuleLoader__.load({
1699
1776
  h('td', {
1700
1777
  key: 'tokens',
1701
1778
  className: 'cp-history-tokens',
1702
- title: `${tokenSummaryExact(entry)}\n${t('historyTokensHint')}`,
1703
- }, tokenSummary(entry)),
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
+ ])),
1704
1784
  h('td', { key: 'load', className: 'cp-history-load' }, [
1705
1785
  h('span', { key: 'first', className: 'cp-history-load-row' }, [
1706
1786
  h('span', { key: 'k', className: 'cp-muted' }, t('historyFirstChunk')),
@@ -1953,6 +2033,7 @@ window.__ModuleLoader__.load({
1953
2033
  pinModel: props.pinModel,
1954
2034
  setModelVisible: props.setModelVisible,
1955
2035
  setToolsExposed: props.setToolsExposed,
2036
+ setBudget: props.setBudget,
1956
2037
  setModelsVisibility: props.setModelsVisibility,
1957
2038
  probeModel: props.probeModel,
1958
2039
  validateModel: props.validateModel,
@@ -2010,8 +2091,9 @@ window.__ModuleLoader__.load({
2010
2091
  // Everything below the accounts card reads through the key that card
2011
2092
  // stores, so none of it exists until there is one.
2012
2093
  data === null || !ready ? null : h(ModelsCard, { key: 'models', t, data, disabled, busy: state.busy, actions }),
2013
- // Route-wide switches last: they are not about one model or account, and
2094
+ // Route-wide settings last: they are not about one model or account, and
2014
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 }),
2015
2097
  data === null ? null : h('div', { key: 'tools', className: 'cp-card' }, [
2016
2098
  h('label', { key: 'head', className: 'cp-row cp-tools-label' }, [
2017
2099
  h('input', {
package/lib/index.js CHANGED
@@ -160,6 +160,11 @@ export const Config = z.object({
160
160
  * larger, and reporting that here reserves it on every request — at the shipped
161
161
  * 384k figure a 1M window compacts at 55% instead of the 80% the threshold ratio
162
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.
163
168
  */
164
169
  defaultMaxTokens: z.number().default(DEFAULT_MAX_TOKENS),
165
170
  streamIdleTimeoutMs: z.number().default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
package/lib/panel.js CHANGED
@@ -130,6 +130,9 @@ export function createPanel({ control, engine, store }) {
130
130
  provider: control.providerName,
131
131
  displayName: control.displayName,
132
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),
133
136
  settingsAvailable: control.settingsAvailable(),
134
137
  accountMode: config.accountMode === 'roundrobin' ? 'roundrobin' : 'single',
135
138
  activeAccount: String(config.activeAccount ?? ''),
@@ -449,6 +452,21 @@ export function createPanel({ control, engine, store }) {
449
452
  return await modelPin({ model: modelOf(payload), upstreams: [], exclude: [], sort: 'none', pinMode: 'preferred' })
450
453
  }
451
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
+
452
470
  /** The live request history, newest first. */
453
471
  async function history(payload) {
454
472
  const limit = Number.isSafeInteger(payload?.limit) && payload.limit > 0 ? Math.min(payload.limit, 100) : 25
@@ -473,6 +491,7 @@ export function createPanel({ control, engine, store }) {
473
491
  cacheReadTokens: Number(entry.usage?.cacheReadTokens ?? 0),
474
492
  reasoningTokens: Number(entry.usage?.reasoningTokens ?? 0),
475
493
  },
494
+ cacheHit: cacheHitOf(entry.usage),
476
495
  effort: String(entry.effort ?? ''),
477
496
  stream: entry.stream === true,
478
497
  error: firstLine(entry.error, 200),
@@ -490,6 +509,23 @@ export function createPanel({ control, engine, store }) {
490
509
  * The response carries `restartRequired` so the panel can say so instead of
491
510
  * implying the switch already took effect.
492
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
+
493
529
  async function toolsExpose(payload) {
494
530
  if (typeof payload?.value !== 'boolean') throw new Error('this action needs a boolean `value`')
495
531
  await control.updateConfig({ exposeTools: payload.value })
@@ -514,6 +550,7 @@ export function createPanel({ control, engine, store }) {
514
550
  'model.reset': modelReset,
515
551
  'models.refresh': modelsRefresh,
516
552
  'tools.expose': toolsExpose,
553
+ 'budget.set': budgetSet,
517
554
  history,
518
555
  }
519
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
- const routing = message?.provider_metadata?.gateway?.routing ?? payload?.provider_metadata?.gateway?.routing ?? {}
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cline-pass",
3
- "version": "0.2.11",
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",