@goodandready/dsh-clinebot 0.5.33 → 0.6.1
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 +32 -0
- package/README.md +63 -36
- package/README.ru.md +3 -1
- package/README.zh.md +3 -1
- package/lib/adapter-messages.js +237 -0
- package/lib/adapter-stream.js +292 -0
- package/lib/adapter.js +616 -0
- package/lib/client.js +2634 -922
- package/lib/cline-client.js +15 -6
- package/lib/config.js +266 -233
- package/lib/credential-refs.js +104 -84
- package/lib/index.js +569 -502
- package/lib/model-prices.js +117 -0
- package/lib/provider-sync.js +404 -387
- package/lib/proxy-token.js +186 -186
- package/lib/routes/accounts.js +523 -403
- package/lib/routes/models.js +2 -1
- package/lib/routes/settings.js +81 -2
- package/lib/services.js +55 -0
- package/lib/stats-storage.js +378 -165
- package/lib/transport/cline-transport.js +314 -0
- package/lib/tui-settings.js +158 -0
- package/package.json +113 -77
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ 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.1] - 2026-10-21
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **ClineBot-only session cost**: the composer now reads the plugin's per-session `bySession` bucket instead of the host's session-wide `tokenUsage`. A conversation that also used OpenCode can no longer inflate the ClineBot estimate.
|
|
12
|
+
- **Last request in the tooltip**: each session bucket retains the latest ClineBot request; the native token pill title now shows the ClineBot session cost and the last request cost.
|
|
13
|
+
- **Free estimates**: priced requests that cost zero render as `Free` instead of `$0.0000`.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `GET /dsh-clinebot/stats/session?sessionId=...` returns the ClineBot-only aggregate for one DSH session, including the last request.
|
|
17
|
+
|
|
18
|
+
## [0.6.0] - 2026-10-20
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Native `ctx.llm` adapter**: `ClineBotAdapter` now owns streaming, model metadata, reasoning effort, pre-first-token account failover, retry policy and provider status. The legacy `llm-pi-ai` provider + loopback proxy is no longer required for chat.
|
|
22
|
+
- **Quota panel**: the sidebar opens a center-column `Quota / Analytics` panel through `layout.selectPanel`, with header refresh/close, account tabs, 120s auto-refresh and explicit `no key` / `endpoint unreachable` / `not registered` states. A modal fallback is used when the host panel API is unavailable.
|
|
23
|
+
- **Three fixed quota rows**: the sidebar always shows `5-Hour`, `Weekly` and `Monthly` limits with reset countdowns, `—` for unavailable windows and the active account in the tooltip.
|
|
24
|
+
- **Analytics v2**: persistent stats now track cache read/write tokens, estimated cost, TTFT, latency and bounded per-session buckets. The panel adds 7/30/90-day ranges, a weekday×hour request heatmap, peak-hour summary, by-hour strip and model filter; `GET /dsh-clinebot/analytics` exposes the aggregates and the store keeps 95 days with best-effort backfill.
|
|
25
|
+
- **Native composer cost**: the session estimate is appended to the host token-usage pill (`N tok · Cache hit X% · ≈ $...` / `Free`) instead of a second dock pill; its tooltip breaks down input, output, cache and reasoning tokens. The cost read-out is scoped to ClineBot model selections so OpenCode sessions keep their own provider estimate.
|
|
26
|
+
- **Local price table**: `lib/model-prices.js` resolves per-model estimates with user overrides and `estimate` / `override` provenance; `GET /dsh-clinebot/prices` feeds the composer. Unknown models display `unpriced` instead of `$0`.
|
|
27
|
+
- **dsh-TUI settings**: `ClineBot` section for API key, base URL, plan filtering, web search, sidebar quota, active account, context window and output cap.
|
|
28
|
+
- **Account surface**: live per-account usage, inline rename, clear key, pin/unpin and delete with inline confirmation; `GET /dsh-clinebot/accounts/usage`, `POST /accounts/rename`, `POST /accounts/key/clear`.
|
|
29
|
+
- **Calm settings UI**: staged single-save configuration, collapsible groups, SVG stroke icons and native primitives; no emojis in the client bundle. Neutral surfaces, compact tables and 8px progress bars follow the OpenCode provider visual language.
|
|
30
|
+
- **Coverage**: 240 tests total (238 pass, 2 platform skips, 0 failures); client bundle 251.8 KB, below the 256 KiB DSH Store limit.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- Retargeted to **DSH `0.2.1-rc.2`** (`engines.dsh`) with peer dependencies on `dsh-llm`, `dsh-attachment`, `dsh-credentials`, `dsh-settings`, `dsh-commands` and `dsh-web`.
|
|
34
|
+
- New installs default to `transport: "adapter"`. Settings, models and account actions are grouped; model and account ops stay immediate.
|
|
35
|
+
- Composer cost now reads the live price endpoint and respects `showComposerCost`; `/stats/export?detailed=1` adds cache, cost, TTFT and latency columns.
|
|
36
|
+
|
|
37
|
+
### Deprecated
|
|
38
|
+
- `transport: "pi-ai"`, `proxyMode`, `lib/proxy-server.js`, `lib/proxy-token.js`, `lib/routes/proxy.js` and the `llm/stream` hook remain for one release as a compatibility fallback. They will be removed in 0.7.0.
|
|
39
|
+
|
|
8
40
|
## [0.5.33] - 2026-10-19
|
|
9
41
|
|
|
10
42
|
### Security
|
package/README.md
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
<h3>Native ClineBot / ClinePass Provider Companion for DeepSeek Harness</h3>
|
|
6
6
|
|
|
7
|
+
<p><b>0.6.0</b> moves chat onto a native <code>ctx.llm</code> adapter. Requires DSH <code>0.2.1-rc.2</code>. The legacy <code>llm-pi-ai</code> + loopback-proxy path remains available behind <code>transport: "pi-ai"</code> for one release.</p>
|
|
8
|
+
|
|
7
9
|
<p align="center">
|
|
8
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>
|
|
9
11
|
<a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-clinebot.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
12
|
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
-
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-
|
|
13
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-22%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
14
|
</p>
|
|
13
15
|
|
|
14
16
|
<!-- Author Showcase Link -->
|
|
@@ -66,59 +68,73 @@ Integrating ClinePass into DeepSeek Harness (DSH) natively poses key challenges:
|
|
|
66
68
|
```mermaid
|
|
67
69
|
graph LR
|
|
68
70
|
subgraph UI [DSH Web Interface]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
StatsCard["Session Metrics Telemetry"]
|
|
71
|
+
Settings["Settings -> ClineBot"]
|
|
72
|
+
Dashboard["Quota / Analytics panel"]
|
|
73
|
+
Composer["Composer cost read-out"]
|
|
74
|
+
Picker["Model picker"]
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
subgraph PluginHost [dsh-clinebot
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
subgraph PluginHost [dsh-clinebot Runtime]
|
|
78
|
+
Routes["HTTP: /dsh-clinebot/*"]
|
|
79
|
+
Adapter["lib/adapter.js (ctx.llm LlmAdapter)"]
|
|
80
|
+
Transport["lib/transport/cline-transport.js"]
|
|
81
|
+
Pool["account-pool + session-router"]
|
|
82
|
+
Stats["stats-storage v2 + model-prices"]
|
|
83
|
+
TUI["dsh-TUI settings section"]
|
|
81
84
|
end
|
|
82
85
|
|
|
83
|
-
subgraph DSHCore [DeepSeek Harness
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
subgraph DSHCore [DeepSeek Harness 0.2.1-rc.2]
|
|
87
|
+
Llm["ctx.llm"]
|
|
88
|
+
Credentials["ctx.credentials"]
|
|
89
|
+
Web["ctx.web / settings / commands"]
|
|
86
90
|
end
|
|
87
91
|
|
|
88
|
-
subgraph Upstream [
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
subgraph Upstream [ClinePass]
|
|
93
|
+
Chat["api.cline.bot/api/v1/chat/completions"]
|
|
94
|
+
Quota["users/me/plan/usage-limits"]
|
|
95
|
+
Plan["users/me/plan"]
|
|
92
96
|
end
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
Settings --> Routes
|
|
99
|
+
Dashboard --> Routes
|
|
100
|
+
Composer --> Routes
|
|
101
|
+
Picker --> Llm
|
|
102
|
+
Llm --> Adapter
|
|
103
|
+
Adapter --> Pool
|
|
104
|
+
Adapter --> Transport
|
|
105
|
+
Transport --> Chat
|
|
106
|
+
Adapter --> Stats
|
|
107
|
+
Routes --> Stats
|
|
108
|
+
Routes --> Credentials
|
|
109
|
+
TUI --> Routes
|
|
110
|
+
Adapter --> Quota
|
|
111
|
+
Adapter --> Plan
|
|
104
112
|
```
|
|
105
113
|
|
|
106
114
|
---
|
|
107
115
|
|
|
108
116
|
## ✨ Features & Module Breakdown
|
|
109
117
|
|
|
118
|
+
* **`lib/adapter.js`**:
|
|
119
|
+
Registers the `ClineBotAdapter` directly on `ctx.llm`: streaming, model catalogue, reasoning effort, pre-first-token account failover, retry policy, usage accounting and provider metadata.
|
|
120
|
+
* **`lib/transport/cline-transport.js`**:
|
|
121
|
+
OpenAI-compatible SSE transport with connect/idle timeouts, abort handling and upstream error classification. The legacy proxy imports the same parser.
|
|
110
122
|
* **`lib/models.js`**:
|
|
111
|
-
|
|
123
|
+
Dynamic subscription catalogue, curated fallback properties, reasoning efforts, custom models and plan parsing (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
|
|
124
|
+
* **`lib/model-prices.js`**:
|
|
125
|
+
Explicit local cost estimates with per-model overrides. Unknown models stay `unpriced` instead of being reported as $0.
|
|
126
|
+
* **`lib/stats-storage.js`**:
|
|
127
|
+
Persistent JSON telemetry v2: cache buckets, estimated cost, TTFT/latency, sessions and day/month/model/account aggregates. v1 files upgrade on read.
|
|
128
|
+
* **`lib/account-pool.js` / `lib/session-router.js`**:
|
|
129
|
+
Credential discovery, usage-aware least-used and round-robin rotation, sticky sessions and cooldown recovery.
|
|
112
130
|
* **`lib/cline-client.js`**:
|
|
113
|
-
|
|
114
|
-
* `sessionStats` / `recordSessionRequest`: in-memory telemetry recording requests count, tokens, latency, and timestamps.
|
|
115
|
-
* `saveCredentialKey`: writes credentials directly into `~/.dsh/.credentials.yaml`.
|
|
116
|
-
* `smokeChat`: tests latency via non-streaming ping and updates session metrics.
|
|
117
|
-
* `buildPiAiProvider`: builds the DSH `llm-pi-ai` structure (`api: 'openai-completions'`) with full reasoning effort protocol compliance (`compat.supportsReasoningEffort: true`).
|
|
131
|
+
`fetchUsageLimits`, credential save, smoke test, model cache and the legacy `llm-pi-ai` provider builder used only by `transport: "pi-ai"`.
|
|
118
132
|
* **`lib/index.js`**:
|
|
119
|
-
Cordis
|
|
133
|
+
Cordis lifecycle: adapter registration, optional HTTP routes, quota warnings, `/cline` command, migration of the legacy provider and TUI section registration.
|
|
134
|
+
* **`lib/tui-settings.js`**:
|
|
135
|
+
`dsh-TUI` section for key, base URL, model filters, active account, context window and output cap.
|
|
120
136
|
* **`lib/client.js`**:
|
|
121
|
-
|
|
137
|
+
Native `settings.section` plus provider card, sidebar meter, center-column quota/analytics panel (modal fallback) and composer cost read-out. No emojis, no full-page panel swap.
|
|
122
138
|
|
|
123
139
|
---
|
|
124
140
|
|
|
@@ -128,8 +144,19 @@ graph LR
|
|
|
128
144
|
dsh plugin --profile web add @goodandready/dsh-clinebot
|
|
129
145
|
```
|
|
130
146
|
|
|
147
|
+
Requires **DSH `0.2.1-rc.2`** and Node 22+. New installs use `transport: "adapter"` and register directly with `ctx.llm`. Set `transport: "pi-ai"` only if you still need the legacy `llm-pi-ai` provider and loopback proxy for one release.
|
|
148
|
+
|
|
131
149
|
Restart your DeepSeek Harness instance and refresh the browser.
|
|
132
150
|
|
|
151
|
+
### 0.6.0 endpoints
|
|
152
|
+
|
|
153
|
+
| Endpoint | Purpose |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `GET /dsh-clinebot/analytics?groupBy=model\|day\|session&from=&to=` | Aggregated requests, tokens, estimated cost, success rate and TTFT |
|
|
156
|
+
| `GET /dsh-clinebot/prices` | Local price table used by the composer read-out |
|
|
157
|
+
| `GET /dsh-clinebot/accounts/usage` | Per-account quota snapshot |
|
|
158
|
+
| `GET /dsh-clinebot/stats/export?detailed=1` | CSV with cache, cost, TTFT and latency columns |
|
|
159
|
+
|
|
133
160
|
---
|
|
134
161
|
|
|
135
162
|
## 💬 Slash-Command `/cline`
|
package/README.ru.md
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
<h3>Нативное подключение провайдера ClineBot / ClinePass для DeepSeek Harness</h3>
|
|
6
6
|
|
|
7
|
+
<p><b>0.6.0</b> переводит чат на нативный адаптер <code>ctx.llm</code>. Требуется DSH <code>0.2.1-rc.2</code>. Старый путь <code>llm-pi-ai</code> + loopback-прокси остаётся за флагом <code>transport: "pi-ai"</code> на один релиз.</p>
|
|
8
|
+
|
|
7
9
|
<p align="center">
|
|
8
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>
|
|
9
11
|
<a href="../LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-clinebot.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
12
|
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
-
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-
|
|
13
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-22%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
14
|
</p>
|
|
13
15
|
|
|
14
16
|
<!-- Обязательная кнопка перехода на витрину всех проектов -->
|
package/README.zh.md
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
<h3>适用于 DeepSeek Harness 的 ClineBot / ClinePass 原生模型提供商伴侣插件</h3>
|
|
6
6
|
|
|
7
|
+
<p><b>0.6.0</b> 将聊天迁移到原生 <code>ctx.llm</code> 适配器。需要 DSH <code>0.2.1-rc.2</code>。旧的 <code>llm-pi-ai</code> + 回环代理路径保留一个版本,通过 <code>transport: "pi-ai"</code> 启用。</p>
|
|
8
|
+
|
|
7
9
|
<p align="center">
|
|
8
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>
|
|
9
11
|
<a href="../LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-clinebot.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
12
|
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
-
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-
|
|
13
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-22%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
14
|
</p>
|
|
13
15
|
|
|
14
16
|
<!-- 作者所有项目展示页面链接 -->
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate DSH request messages and tools into ClinePass's OpenAI-compatible
|
|
3
|
+
* wire shape. Files are projected to text handles; images are resolved through
|
|
4
|
+
* the attachment seam into request-sized bytes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const IMAGE_MAX_EDGE = 1568
|
|
8
|
+
const IMAGE_MAX_BYTES = 1024 * 1024
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Concatenate text blocks.
|
|
12
|
+
* @param {readonly Record<string, unknown>[]} blocks
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function textOf(blocks) {
|
|
16
|
+
if (!Array.isArray(blocks)) return ''
|
|
17
|
+
return blocks
|
|
18
|
+
.filter((block) => block && block.type === 'text' && typeof block.text === 'string')
|
|
19
|
+
.map((block) => block.text)
|
|
20
|
+
.join('')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Stable placeholder for one file block.
|
|
25
|
+
* @param {Record<string, unknown>} block
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function fileText(block) {
|
|
29
|
+
const ref = block?.attachment && typeof block.attachment === 'object' ? block.attachment : null
|
|
30
|
+
if (!ref) return '[File omitted]'
|
|
31
|
+
const name = typeof ref.name === 'string' && ref.name ? ref.name : 'attachment'
|
|
32
|
+
const bytes = Number.isFinite(Number(ref.bytes)) ? Number(ref.bytes) : null
|
|
33
|
+
return `[File: ${name}${bytes === null ? '' : `, ${bytes} bytes`}]`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Placeholder for an image that cannot be resolved or was deliberately offloaded.
|
|
38
|
+
* @param {Record<string, unknown>} block
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function imagePlaceholder(block) {
|
|
42
|
+
const ref = block?.attachment && typeof block.attachment === 'object' ? block.attachment : null
|
|
43
|
+
const name = typeof ref?.name === 'string' && ref.name ? ref.name : 'image'
|
|
44
|
+
const mediaType = typeof ref?.mediaType === 'string' ? ref.mediaType : 'image'
|
|
45
|
+
if (block?.offloaded === true) {
|
|
46
|
+
return `[Image: ${name} (${mediaType}) — previously analyzed and omitted from this request]`
|
|
47
|
+
}
|
|
48
|
+
return `[Image: ${name} (${mediaType})]`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Request-image target for ClinePass's gateway.
|
|
53
|
+
* @param {Record<string, unknown>} ref
|
|
54
|
+
* @returns {{ width: number, height: number, maxBytes: number }}
|
|
55
|
+
*/
|
|
56
|
+
function imageTarget(ref) {
|
|
57
|
+
const width = Math.max(1, Number(ref?.width) || IMAGE_MAX_EDGE)
|
|
58
|
+
const height = Math.max(1, Number(ref?.height) || IMAGE_MAX_EDGE)
|
|
59
|
+
const scale = Math.min(1, IMAGE_MAX_EDGE / Math.max(width, height))
|
|
60
|
+
return {
|
|
61
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
62
|
+
height: Math.max(1, Math.round(height * scale)),
|
|
63
|
+
maxBytes: IMAGE_MAX_BYTES,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve one image block to a data URL.
|
|
69
|
+
* @param {Record<string, unknown>} block
|
|
70
|
+
* @param {unknown} attachments
|
|
71
|
+
* @param {AbortSignal | undefined} signal
|
|
72
|
+
* @returns {Promise<string>}
|
|
73
|
+
*/
|
|
74
|
+
async function imageToUrl(block, attachments, signal) {
|
|
75
|
+
if (block?.offloaded === true) return imagePlaceholder(block)
|
|
76
|
+
const ref = block?.attachment && typeof block.attachment === 'object' ? block.attachment : null
|
|
77
|
+
if (!ref) return imagePlaceholder(block)
|
|
78
|
+
if (!attachments || typeof attachments.readImageRequest !== 'function') {
|
|
79
|
+
return imagePlaceholder(block)
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const requestImage = await attachments.readImageRequest(ref, imageTarget(ref), signal)
|
|
83
|
+
if (!requestImage?.data) return imagePlaceholder(block)
|
|
84
|
+
const base64 = Buffer.from(requestImage.data).toString('base64')
|
|
85
|
+
return `data:${requestImage.mediaType || ref.mediaType || 'image/png'};base64,${base64}`
|
|
86
|
+
} catch {
|
|
87
|
+
return imagePlaceholder(block)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build OpenAI-compatible content for one user/developer message.
|
|
93
|
+
* @param {readonly Record<string, unknown>[]} blocks
|
|
94
|
+
* @param {unknown} attachments
|
|
95
|
+
* @param {AbortSignal | undefined} signal
|
|
96
|
+
* @returns {Promise<string | Record<string, unknown>[]>}
|
|
97
|
+
*/
|
|
98
|
+
async function userContent(blocks, attachments, signal) {
|
|
99
|
+
const content = []
|
|
100
|
+
let hasImage = false
|
|
101
|
+
for (const block of Array.isArray(blocks) ? blocks : []) {
|
|
102
|
+
if (!block || typeof block !== 'object') continue
|
|
103
|
+
if (block.type === 'text' && typeof block.text === 'string' && block.text !== '') {
|
|
104
|
+
content.push({ type: 'text', text: block.text })
|
|
105
|
+
} else if (block.type === 'image') {
|
|
106
|
+
hasImage = true
|
|
107
|
+
content.push({ type: 'image_url', image_url: { url: await imageToUrl(block, attachments, signal) } })
|
|
108
|
+
} else if (block.type === 'file') {
|
|
109
|
+
content.push({ type: 'text', text: fileText(block) })
|
|
110
|
+
} else if (block.type === 'tool-result') {
|
|
111
|
+
const resultText = textOf(block.content)
|
|
112
|
+
if (resultText) content.push({ type: 'text', text: resultText })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!hasImage) {
|
|
116
|
+
return content.filter((part) => part.type === 'text').map((part) => part.text).join('\n')
|
|
117
|
+
}
|
|
118
|
+
return content
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build an assistant message with text, reasoning replay, and tool calls.
|
|
123
|
+
* @param {Record<string, unknown>} message
|
|
124
|
+
* @param {unknown} attachments
|
|
125
|
+
* @param {AbortSignal | undefined} signal
|
|
126
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
127
|
+
*/
|
|
128
|
+
async function assistantMessage(message, attachments, signal) {
|
|
129
|
+
const blocks = Array.isArray(message.content) ? message.content : []
|
|
130
|
+
const text = textOf(blocks)
|
|
131
|
+
const reasoning = blocks
|
|
132
|
+
.filter((block) => block && block.type === 'reasoning' && typeof block.text === 'string')
|
|
133
|
+
.map((block) => block.text)
|
|
134
|
+
.join('')
|
|
135
|
+
const toolCalls = blocks
|
|
136
|
+
.filter((block) => block && block.type === 'tool-call')
|
|
137
|
+
.map((block) => ({
|
|
138
|
+
id: String(block.id || ''),
|
|
139
|
+
type: 'function',
|
|
140
|
+
function: {
|
|
141
|
+
name: String(block.name || ''),
|
|
142
|
+
arguments: typeof block.arguments === 'string' ? block.arguments : JSON.stringify(block.arguments ?? {}),
|
|
143
|
+
},
|
|
144
|
+
}))
|
|
145
|
+
|
|
146
|
+
const out = {
|
|
147
|
+
role: 'assistant',
|
|
148
|
+
content: text === '' ? null : text,
|
|
149
|
+
...(reasoning ? { reasoning_content: reasoning } : {}),
|
|
150
|
+
...(toolCalls.length ? { tool_calls: toolCalls } : {}),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Some assistant content may include images. ClinePass's chat shape cannot
|
|
154
|
+
// represent assistant-side images, so they become deterministic text.
|
|
155
|
+
for (const block of blocks) {
|
|
156
|
+
if (block?.type === 'image') {
|
|
157
|
+
out.content = `${out.content || ''}${out.content ? '\n' : ''}${imagePlaceholder(block)}`
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Build the complete OpenAI-compatible payload.
|
|
165
|
+
*
|
|
166
|
+
* @param {{
|
|
167
|
+
* provider: string,
|
|
168
|
+
* model: string,
|
|
169
|
+
* messages: readonly Record<string, unknown>[],
|
|
170
|
+
* system?: string,
|
|
171
|
+
* tools?: readonly Record<string, unknown>[],
|
|
172
|
+
* temperature?: number,
|
|
173
|
+
* maxTokens?: number,
|
|
174
|
+
* stop?: readonly string[],
|
|
175
|
+
* signal?: AbortSignal,
|
|
176
|
+
* }} options
|
|
177
|
+
* @param {{ attachments?: unknown }} [deps]
|
|
178
|
+
* @returns {Promise<{ messages: Record<string, unknown>[], tools: Record<string, unknown>[] }>}
|
|
179
|
+
*/
|
|
180
|
+
export async function buildOpenAiPayload(options, { attachments } = {}) {
|
|
181
|
+
const messages = []
|
|
182
|
+
|
|
183
|
+
if (typeof options.system === 'string' && options.system !== '') {
|
|
184
|
+
messages.push({ role: 'system', content: options.system })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const message of Array.isArray(options.messages) ? options.messages : []) {
|
|
188
|
+
if (!message || typeof message !== 'object') continue
|
|
189
|
+
const role = String(message.role || '')
|
|
190
|
+
const blocks = Array.isArray(message.content) ? message.content : []
|
|
191
|
+
|
|
192
|
+
if (role === 'system') {
|
|
193
|
+
const text = textOf(blocks)
|
|
194
|
+
if (text !== '') messages.push({ role: 'system', content: text })
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
if (role === 'developer') {
|
|
198
|
+
const text = textOf(blocks)
|
|
199
|
+
if (text !== '') messages.push({ role: 'system', content: text })
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
if (role === 'assistant') {
|
|
203
|
+
messages.push(await assistantMessage(message, attachments, options.signal))
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
if (role === 'tool') {
|
|
207
|
+
const callId = message.toolCallId
|
|
208
|
+
|| message.source?.callId
|
|
209
|
+
|| blocks.find((block) => block?.type === 'tool-result')?.toolCallId
|
|
210
|
+
|| ''
|
|
211
|
+
messages.push({
|
|
212
|
+
role: 'tool',
|
|
213
|
+
tool_call_id: String(callId),
|
|
214
|
+
content: textOf(blocks) || (message.isError ? '[tool error]' : ''),
|
|
215
|
+
})
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
// `user` and identity-free request inputs.
|
|
219
|
+
const content = await userContent(blocks, attachments, options.signal)
|
|
220
|
+
messages.push({ role: 'user', content })
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const tools = (Array.isArray(options.tools) ? options.tools : [])
|
|
224
|
+
.filter((tool) => tool && typeof tool === 'object')
|
|
225
|
+
.map((tool) => ({
|
|
226
|
+
type: 'function',
|
|
227
|
+
function: {
|
|
228
|
+
name: String(tool.name || ''),
|
|
229
|
+
description: String(tool.description || ''),
|
|
230
|
+
parameters: tool.parameters && typeof tool.parameters === 'object'
|
|
231
|
+
? tool.parameters
|
|
232
|
+
: { type: 'object', properties: {} },
|
|
233
|
+
},
|
|
234
|
+
}))
|
|
235
|
+
|
|
236
|
+
return { messages, tools }
|
|
237
|
+
}
|