@goodandready/dsh-clinebot 0.6.13 → 0.6.15

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/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.15] - 2026-10-21
9
+
10
+ ### Added
11
+ - **Multimodal budgeting** (#257): the adapter now runs `optimizePromptCacheAndBudget`, honors `offloadSeenImages` and `maxPayloadMb`, and records `offloadedImages` in session telemetry.
12
+ - **Model controls** (#258): per-model **Ping** calls `POST /models/probe` and shows TTFT; **Apply visibility** calls `POST /models/visible`.
13
+ - **Price auto-sync switch** (#259): `priceAutoSync` is available in settings and the price card, and is documented in all three READMEs.
14
+
15
+ ### Fixed
16
+ - **Write-route source gate** (#261): POST/PUT without `Origin`/`Referer` are rejected fail-closed, closing the dsh-lanmode loopback-bridge write bypass.
17
+ - Removed test-only exports (`getDefaultModelIds`, `resetPriceSyncState`, `clearAccountCooldown`, `releaseSession`) and wired `configReader` into the host path.
18
+ - Added `.gitattributes` (`* text=auto eol=lf`) and normalized tracked files.
19
+
20
+ ## [0.6.14] - 2026-10-21
21
+
22
+ ### Fixed
23
+ - **Settings source gate**: remote clients can no longer pass by spoofing `Sec-Fetch-Site`; `X-Forwarded-Host` is trusted only from a loopback reverse proxy. Added regression cases for both bypasses.
24
+ - **Theme regression**: replaced 58 hardcoded colors in the client stylesheet with DSH theme tokens and `color-mix`, restoring light/dark compatibility and matching `DESIGN.md`.
25
+
26
+ ### Changed
27
+ - Extracted attempt telemetry into `lib/adapter-telemetry.js`; `lib/adapter.js` is back under the 600-line budget.
28
+ - The decomposition test now scans every server module instead of a hand-maintained allowlist.
29
+
30
+ ### Removed
31
+ - Dead/test-only exports `clearAccountEvents`, `getDiscoveredKey` and `clearDiscoveredKeysCache` (with tests adjusted to the production API).
32
+ - Annotated intentional best-effort catches and removed stale `.tgz` artifacts from the workspace.
33
+
8
34
  ## [0.6.13] - 2026-10-21
9
35
 
10
36
  ### Added
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h3>Native ClineBot / ClinePass Provider Companion for DeepSeek Harness</h3>
6
6
 
7
- <p><b>0.6.13</b> automatically refreshes public prices when new models appear. Chat runs on the native <code>ctx.llm</code> adapter and requires DSH <code>0.2.1-rc.2</code>; the legacy <code>llm-pi-ai</code>, loopback proxy and <code>llm/stream</code> path are removed.</p>
7
+ <p><b>0.6.15</b> connects multimodal budgeting, exposes model probe/visibility controls, adds the price auto-sync switch and closes the remaining audit hygiene/security findings. Chat runs on the native <code>ctx.llm</code> adapter and requires DSH <code>0.2.1-rc.2</code>; the legacy <code>llm-pi-ai</code>, loopback proxy and <code>llm/stream</code> path are removed.</p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
@@ -122,7 +122,11 @@ graph LR
122
122
  * **`lib/models.js`**:
123
123
  Dynamic subscription catalogue, curated fallback properties, reasoning efforts, custom models and plan parsing (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
124
124
  * **`lib/model-prices.js`**:
125
- Explicit local cost estimates with per-model overrides. Unknown models stay `unpriced` instead of being reported as $0.
125
+ Explicit local cost estimates with per-model overrides. Unknown models stay `unpriced` instead of being reported as $0. When a newly discovered model has no price, the optional `priceAutoSync` setting fetches the public models.dev catalog (at most once per missing set every 6 hours); turn it off in the price card or set `priceAutoSync: false` while keeping the manual Sync button.
126
+ * **`lib/multimodal-budget.js`**:
127
+ Offloads images from earlier turns into stable text placeholders and enforces `maxPayloadMb`, reducing prompt-cache churn and oversized ClinePass requests. `offloadedImages` is included in session telemetry.
128
+ * **Model tools in Settings → ClineBot → Models**:
129
+ The per-model **Ping** button calls `POST /models/probe` and shows TTFT; **Apply visibility** calls `POST /models/visible` after choosing the visible models and plan-only filter.
126
130
  * **`lib/stats-storage.js`**:
127
131
  Persistent JSON telemetry v2: cache buckets, estimated cost, TTFT/latency, sessions and day/month/model/account aggregates. v1 files upgrade on read.
128
132
  * **`lib/account-pool.js` / `lib/session-router.js`**:
@@ -199,6 +203,7 @@ dsh-clinebot:
199
203
  statsPath: ~/.dsh/clinebot-stats.json
200
204
  priceCatalogUrl: https://models.dev/api.json
201
205
  priceCatalogPath: ~/.dsh/clinebot-prices.json
206
+ priceAutoSync: true
202
207
  eventLogPath: ~/.dsh/clinebot-events.json
203
208
  statsRetentionDays: 95
204
209
  dailyBudgetUsd: 0
@@ -226,6 +231,7 @@ dsh-clinebot:
226
231
  | `statsPath` | `string` | `"~/.dsh/clinebot-stats.json"` | Persistent on-disk path for token usage analytics |
227
232
  | `priceCatalogUrl` | `string` | `"https://models.dev/api.json"` | Public price catalog used by the optional sync |
228
233
  | `priceCatalogPath` | `string` | `"~/.dsh/clinebot-prices.json"` | Local cache for the public price catalog |
234
+ | `priceAutoSync` | `boolean` | `true` | Fetch models.dev for newly discovered models without a price; the manual Sync button remains available when off |
229
235
  | `eventLogPath` | `string` | `"~/.dsh/clinebot-events.json"` | Ring buffer for account cooldown/recovery/routing history |
230
236
  | `statsRetentionDays` | `number` | `95` | Retention for day/hour/session telemetry; 0 keeps everything |
231
237
  | `dailyBudgetUsd` | `number` | `0` | Optional estimated daily spend guard; 0 disables it |
package/README.ru.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h3>Нативное подключение провайдера ClineBot / ClinePass для DeepSeek Harness</h3>
6
6
 
7
- <p><b>0.6.13</b> автоматически догружает публичные цены при появлении новых моделей. Чат работает через нативный адаптер <code>ctx.llm</code>, требуется DSH <code>0.2.1-rc.2</code>; старые <code>llm-pi-ai</code>, loopback-прокси и <code>llm/stream</code> удалены.</p>
7
+ <p><b>0.6.15</b> подключает бюджетирование мультимодальных запросов, добавляет замер моделей и управление видимостью, переключатель автосинхронизации цен и закрывает оставшиеся находки аудита. Чат работает через нативный адаптер <code>ctx.llm</code>, требуется DSH <code>0.2.1-rc.2</code>; старые <code>llm-pi-ai</code>, loopback-прокси и <code>llm/stream</code> удалены.</p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
@@ -108,6 +108,7 @@ graph LR
108
108
  ## ✨ Структура модулей и возможности
109
109
 
110
110
  * **`lib/models.js`**: динамический каталог моделей подписки и встроенных шаблонов с поддержкой уровней рассуждения (`Off` / `Low` / `Medium` / `High` / `Max`), контекстных лимитов, пользовательских моделей (`customModels`) и парсинга тарифа (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
111
+ * **Цены и модели**: кнопка **Ping** у модели вызывает `POST /models/probe` и показывает TTFT; кнопка **Применить видимость** сохраняет выбор видимых моделей и фильтр тарифа через `POST /models/visible`. Публичные цены могут автоматически подгружаться из models.dev для новых моделей; это отключается переключателем `priceAutoSync` (или `priceAutoSync: false`), ручная синхронизация остаётся доступной.
111
112
  * **`lib/cline-client.js`**:
112
113
  * `fetchUsageLimits`: параллельный опрос `GET /users/me/plan/usage-limits`, `GET /users/me/plan` и `GET /users/me` с кэшированием в памяти.
113
114
  * `sessionStats` / `recordSessionRequest`: счетчики сессии (запросы, токены, задержка, время).
package/README.zh.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h3>适用于 DeepSeek Harness 的 ClineBot / ClinePass 原生模型提供商伴侣插件</h3>
6
6
 
7
- <p><b>0.6.13</b> 在出现新模型时自动刷新公开价格。聊天使用原生 <code>ctx.llm</code> 适配器,需要 DSH <code>0.2.1-rc.2</code>;旧的 <code>llm-pi-ai</code>、回环代理和 <code>llm/stream</code> 已删除。</p>
7
+ <p><b>0.6.15</b> 接入多模态预算、模型延迟/可见性控制、价格自动同步开关,并修复其余审计问题。聊天使用原生 <code>ctx.llm</code> 适配器,需要 DSH <code>0.2.1-rc.2</code>;旧的 <code>llm-pi-ai</code>、回环代理和 <code>llm/stream</code> 已删除。</p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
@@ -108,6 +108,7 @@ graph LR
108
108
  ## ✨ 核心模块与功能
109
109
 
110
110
  * **`lib/models.js`**:管理动态订阅模型目录与内置精选模板(提供完整的 `Off` / `Low` / `Medium` / `High` / `Max` 思考强度映射、上游 200k 上下文容量标注、自定义模型支持)以及套餐模型动态解析器(`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`)。
111
+ * **价格与模型**:模型行中的 **Ping** 调用 `POST /models/probe` 并显示 TTFT;**应用可见模型** 通过 `POST /models/visible` 保存可见模型和套餐过滤。新模型缺少价格时可通过 `priceAutoSync` 开关自动从 models.dev 获取公开价格;关闭后仍可使用手动同步。
111
112
  * **`lib/cline-client.js`**:
112
113
  * `fetchUsageLimits`:高效并发轮询 `GET /users/me/plan/usage-limits`、`GET /users/me/plan` 与 `GET /users/me` 并进行内存缓存。
113
114
  * `sessionStats` / `recordSessionRequest`:内存级会话度量记录器(请求次数、Token 估算、延迟、时间戳)。
package/lib/access.js CHANGED
@@ -24,5 +24,17 @@ export function assertTrustedSettingsRequest(req, res, ctx = null) {
24
24
  writeJson(res, 403, { ok: false, error: 'Forbidden' })
25
25
  return false
26
26
  }
27
+
28
+ // A loopback-only caller is not enough for state changes: dsh-lanmode can
29
+ // proxy LAN/Tailscale clients into the host over loopback. Browsers attach
30
+ // Origin/Referer; non-browser writes without one are rejected fail-closed.
31
+ const method = String(req?.method || 'GET').toUpperCase()
32
+ if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS') {
33
+ const origin = req?.headers?.origin || req?.headers?.referer
34
+ if (typeof origin !== 'string' || origin.length === 0) {
35
+ writeJson(res, 403, { ok: false, error: 'Forbidden' })
36
+ return false
37
+ }
38
+ }
27
39
  return true
28
40
  }
@@ -79,7 +79,3 @@ export function flushAccountEvents() {
79
79
  }
80
80
  }
81
81
 
82
- export function clearAccountEvents() {
83
- events = []
84
- flushAccountEvents()
85
- }
@@ -0,0 +1,76 @@
1
+ import { estimateCost } from './model-prices.js'
2
+ import { recordSessionRequest } from './telemetry.js'
3
+ import { recordUsage } from './stats-storage.js'
4
+
5
+ /**
6
+ * Record one attempt's session and persistent telemetry.
7
+ * Kept out of adapter.js so the adapter stays within the decomposition budget.
8
+ */
9
+ export function recordAttemptTelemetry({
10
+ pub,
11
+ accountId,
12
+ model,
13
+ usage,
14
+ isError,
15
+ is429 = false,
16
+ ttftMs,
17
+ latencyMs,
18
+ aborted = false,
19
+ options,
20
+ offloadedImages = 0,
21
+ }) {
22
+ const promptTokens = Number(usage?.promptTokens || 0)
23
+ const completionTokens = Number(usage?.completionTokens || 0)
24
+ const reasoningTokens = Number(usage?.reasoningTokens || 0)
25
+ const totalTokens = Number(usage?.totalTokens || (promptTokens + completionTokens))
26
+ const cost = estimateCost({
27
+ model,
28
+ promptTokens,
29
+ completionTokens,
30
+ reasoningTokens,
31
+ cacheReadTokens: usage?.cacheReadTokens,
32
+ cacheWriteTokens: usage?.cacheWriteTokens,
33
+ overrides: pub.priceOverrides || {},
34
+ })
35
+
36
+ try {
37
+ recordSessionRequest({
38
+ latencyMs,
39
+ ok: !isError,
40
+ aborted,
41
+ promptTokens,
42
+ completionTokens,
43
+ reasoningTokens,
44
+ model,
45
+ offloadedImages,
46
+ ...(isError ? { error: is429 ? 'rate limit' : 'stream error' } : {}),
47
+ })
48
+ } catch {
49
+ /* telemetry must never break inference */
50
+ }
51
+
52
+ try {
53
+ recordUsage({
54
+ model,
55
+ accountId,
56
+ promptTokens,
57
+ completionTokens,
58
+ reasoningTokens,
59
+ totalTokens,
60
+ isError,
61
+ is429,
62
+ statsPath: pub.statsPath,
63
+ sessionId: options.sessionId ? String(options.sessionId) : undefined,
64
+ purpose: options.purpose,
65
+ ttftMs: ttftMs ?? undefined,
66
+ latencyMs,
67
+ cacheReadTokens: usage?.cacheReadTokens,
68
+ cacheWriteTokens: usage?.cacheWriteTokens,
69
+ cacheSavingsUsd: cost.cacheSavingsUsd,
70
+ costUsd: cost.usd,
71
+ priced: cost.priced,
72
+ })
73
+ } catch {
74
+ /* persistent telemetry is best-effort */
75
+ }
76
+ }
package/lib/adapter.js CHANGED
@@ -11,11 +11,11 @@ import { publicConfig } from './config.js'
11
11
  import { getAllModels, findModel, formatModelDescription } from './models.js'
12
12
  import { resolveAccountPool } from './account-pool.js'
13
13
  import { resolveSessionAccount, markAccountCooldown } from './session-router.js'
14
- import { recordSessionRequest } from './telemetry.js'
15
- import { recordUsage, getBudgetSpend } from './stats-storage.js'
14
+ import { recordAttemptTelemetry } from './adapter-telemetry.js'
15
+ import { getBudgetSpend } from './stats-storage.js'
16
16
  import { budgetStatus, budgetErrorMessage } from './budget.js'
17
- import { estimateCost } from './model-prices.js'
18
17
  import { buildOpenAiPayload } from './adapter-messages.js'
18
+ import { optimizePromptCacheAndBudget } from './multimodal-budget.js'
19
19
  import { streamJsonCompletion, streamOpenAiChunks } from './adapter-stream.js'
20
20
  import {
21
21
  abortError,
@@ -264,8 +264,14 @@ export class ClineBotAdapter extends LlmAdapter {
264
264
  const attachments = this.resolveAttachments
265
265
  ? this.resolveAttachments()
266
266
  : attachmentStore(this.ctx)
267
+ const optimized = optimizePromptCacheAndBudget(options.messages, {
268
+ offloadSeenImages: pub.offloadSeenImages,
269
+ maxPayloadBytes: Number(pub.maxPayloadMb || 40) * 1024 * 1024,
270
+ })
271
+ const requestOptions = { ...options, offloadedImages: optimized.offloadedCount }
267
272
  const payload = await buildOpenAiPayload({
268
273
  ...options,
274
+ messages: optimized.messages,
269
275
  model,
270
276
  signal,
271
277
  }, { attachments })
@@ -445,7 +451,7 @@ export class ClineBotAdapter extends LlmAdapter {
445
451
  isError: false,
446
452
  ttftMs,
447
453
  latencyMs: Date.now() - startedAt,
448
- options,
454
+ options: requestOptions,
449
455
  })
450
456
  return
451
457
  }
@@ -474,7 +480,7 @@ export class ClineBotAdapter extends LlmAdapter {
474
480
  isError: false,
475
481
  ttftMs,
476
482
  latencyMs: Date.now() - startedAt,
477
- options,
483
+ options: requestOptions,
478
484
  })
479
485
  return
480
486
  } catch (error) {
@@ -487,7 +493,7 @@ export class ClineBotAdapter extends LlmAdapter {
487
493
  isError: true,
488
494
  ttftMs,
489
495
  latencyMs: Date.now() - startedAt,
490
- options,
496
+ options: requestOptions,
491
497
  aborted: true,
492
498
  })
493
499
  throw abortError()
@@ -513,7 +519,7 @@ export class ClineBotAdapter extends LlmAdapter {
513
519
  is429: normalized.code === 'RATE_LIMIT',
514
520
  ttftMs,
515
521
  latencyMs: Date.now() - startedAt,
516
- options,
522
+ options: requestOptions,
517
523
  })
518
524
  throw normalized
519
525
  }
@@ -549,72 +555,11 @@ export class ClineBotAdapter extends LlmAdapter {
549
555
  * options: Record<string, unknown>,
550
556
  * }} entry
551
557
  */
552
- recordAttempt({
553
- pub,
554
- accountId,
555
- model,
556
- usage,
557
- isError,
558
- is429 = false,
559
- ttftMs,
560
- latencyMs,
561
- aborted = false,
562
- options,
563
- }) {
564
- const promptTokens = Number(usage?.promptTokens || 0)
565
- const completionTokens = Number(usage?.completionTokens || 0)
566
- const reasoningTokens = Number(usage?.reasoningTokens || 0)
567
- const totalTokens = Number(usage?.totalTokens || (promptTokens + completionTokens))
568
- const cost = estimateCost({
569
- model,
570
- promptTokens,
571
- completionTokens,
572
- reasoningTokens,
573
- cacheReadTokens: usage?.cacheReadTokens,
574
- cacheWriteTokens: usage?.cacheWriteTokens,
575
- overrides: pub.priceOverrides || {},
558
+ recordAttempt(entry) {
559
+ recordAttemptTelemetry({
560
+ ...entry,
561
+ offloadedImages: entry?.options?.offloadedImages || 0,
576
562
  })
577
-
578
- try {
579
- recordSessionRequest({
580
- latencyMs,
581
- ok: !isError,
582
- aborted,
583
- promptTokens,
584
- completionTokens,
585
- reasoningTokens,
586
- model,
587
- ...(isError ? { error: is429 ? 'rate limit' : 'stream error' } : {}),
588
- })
589
- } catch {
590
- /* telemetry must never break inference */
591
- }
592
-
593
- try {
594
- recordUsage({
595
- model,
596
- accountId,
597
- promptTokens,
598
- completionTokens,
599
- reasoningTokens,
600
- totalTokens,
601
- isError,
602
- is429,
603
- statsPath: pub.statsPath,
604
- // Extended fields consumed by stats v2 once available.
605
- sessionId: options.sessionId ? String(options.sessionId) : undefined,
606
- purpose: options.purpose,
607
- ttftMs: ttftMs ?? undefined,
608
- latencyMs,
609
- cacheReadTokens: usage?.cacheReadTokens,
610
- cacheWriteTokens: usage?.cacheWriteTokens,
611
- cacheSavingsUsd: cost.cacheSavingsUsd,
612
- costUsd: cost.usd,
613
- priced: cost.priced,
614
- })
615
- } catch {
616
- /* persistent telemetry is best-effort */
617
- }
618
563
  }
619
564
  }
620
565