dsh-cline-pass 0.1.0
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/LICENSE +28 -0
- package/README.en.md +124 -0
- package/README.md +124 -0
- package/cordis.patch.yml +27 -0
- package/lib/adapter.js +620 -0
- package/lib/catalog.js +193 -0
- package/lib/client.js +945 -0
- package/lib/cline.js +194 -0
- package/lib/engine.js +405 -0
- package/lib/index.js +409 -0
- package/lib/panel.js +464 -0
- package/lib/protocol.js +226 -0
- package/lib/store.js +92 -0
- package/lib/tools.js +756 -0
- package/package.json +72 -0
package/lib/tools.js
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-facing `cline_pass_*` management tools.
|
|
3
|
+
*
|
|
4
|
+
* They read and write the same settings section the provider runs from, so a
|
|
5
|
+
* pin written here takes effect on the very next model call.
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-cline-pass/tools
|
|
8
|
+
*/
|
|
9
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
10
|
+
|
|
11
|
+
/** One model-facing text block. */
|
|
12
|
+
function text(value) {
|
|
13
|
+
return [{ type: 'text', text: value }]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Collapse a value into one display line. */
|
|
17
|
+
function firstLine(value, max = 200) {
|
|
18
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim().slice(0, max)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Human label for one upstream availability verdict. */
|
|
22
|
+
const STATUS_LABEL = {
|
|
23
|
+
ok: 'available',
|
|
24
|
+
limited: 'rate-limited',
|
|
25
|
+
bad: 'not-pinnable',
|
|
26
|
+
auth: 'auth-failed',
|
|
27
|
+
unknown: 'unknown',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const statusLabel = (status) => STATUS_LABEL[status] ?? String(status ?? 'unknown')
|
|
31
|
+
|
|
32
|
+
const ACCOUNT_SCHEMA = {
|
|
33
|
+
type: 'object',
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
properties: {
|
|
36
|
+
key: { type: 'string', required: true },
|
|
37
|
+
displayName: { type: 'string', required: true },
|
|
38
|
+
apiKeyEnv: { type: 'string', required: true },
|
|
39
|
+
enabled: { type: 'boolean', required: true },
|
|
40
|
+
keyConfigured: { type: 'boolean', required: true },
|
|
41
|
+
keyHint: { type: 'string', required: true },
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const TRACE_SCHEMA = {
|
|
46
|
+
type: 'object',
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
properties: {
|
|
49
|
+
upstream: { type: 'string', required: true },
|
|
50
|
+
status: { type: 'integer', required: true },
|
|
51
|
+
ms: { type: 'integer', required: true },
|
|
52
|
+
note: { type: 'string', required: true },
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const UPSTREAM_STATUS_SCHEMA = {
|
|
57
|
+
type: 'object',
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
properties: {
|
|
60
|
+
upstream: { type: 'string', required: true },
|
|
61
|
+
status: { type: 'string', required: true },
|
|
62
|
+
ms: { type: 'integer', required: true },
|
|
63
|
+
note: { type: 'string', required: true },
|
|
64
|
+
checkedAt: { type: 'integer', required: true },
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const MODEL_SCHEMA = {
|
|
69
|
+
type: 'object',
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
properties: {
|
|
72
|
+
id: { type: 'string', required: true },
|
|
73
|
+
displayName: { type: 'string', required: true },
|
|
74
|
+
pipeline: { type: 'string', required: true },
|
|
75
|
+
pinnable: { type: 'boolean', required: true },
|
|
76
|
+
pinMode: { type: 'string', required: true },
|
|
77
|
+
sort: { type: 'string', required: true },
|
|
78
|
+
pinned: { type: 'array', required: true, items: { type: 'string' } },
|
|
79
|
+
excluded: { type: 'array', required: true, items: { type: 'string' } },
|
|
80
|
+
upstreams: { type: 'array', required: true, items: { type: 'string' } },
|
|
81
|
+
upstreamStatus: { type: 'array', required: true, items: UPSTREAM_STATUS_SCHEMA },
|
|
82
|
+
lastProvider: { type: 'string', required: true },
|
|
83
|
+
lastMs: { type: 'integer', required: true },
|
|
84
|
+
probedAt: { type: 'integer', required: true },
|
|
85
|
+
validatedAt: { type: 'integer', required: true },
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Project one model from the live configuration plus what the store observed. */
|
|
90
|
+
function projectModel(model, pin, meta) {
|
|
91
|
+
const upstreamStatus = Object.entries(meta?.upstreamStatus ?? {}).map(([upstream, verdict]) => ({
|
|
92
|
+
upstream: String(upstream),
|
|
93
|
+
status: String(verdict?.status ?? 'unknown'),
|
|
94
|
+
ms: Number(verdict?.ms ?? 0),
|
|
95
|
+
note: firstLine(verdict?.note, 160),
|
|
96
|
+
checkedAt: Number(verdict?.checkedAt ?? 0),
|
|
97
|
+
}))
|
|
98
|
+
return {
|
|
99
|
+
id: String(model),
|
|
100
|
+
displayName: String(model).replace(/^cline-pass\//, ''),
|
|
101
|
+
pipeline: String(meta?.pipeline ?? ''),
|
|
102
|
+
pinnable: meta?.pinnable === true,
|
|
103
|
+
pinMode: pin?.pinMode === 'preferred' ? 'preferred' : 'strict',
|
|
104
|
+
sort: String(pin?.sort ?? ''),
|
|
105
|
+
pinned: Array.isArray(pin?.upstreams) ? pin.upstreams.map(String) : [],
|
|
106
|
+
excluded: Array.isArray(pin?.exclude) ? pin.exclude.map(String) : [],
|
|
107
|
+
upstreams: Array.isArray(meta?.upstreams) ? meta.upstreams.map(String) : [],
|
|
108
|
+
upstreamStatus,
|
|
109
|
+
lastProvider: String(meta?.lastProvider ?? ''),
|
|
110
|
+
lastMs: Number(meta?.lastMs ?? 0),
|
|
111
|
+
probedAt: Number(meta?.probedAt ?? 0),
|
|
112
|
+
validatedAt: Number(meta?.validatedAt ?? 0),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Register every `cline_pass_*` tool.
|
|
118
|
+
*
|
|
119
|
+
* @param ctx - host plugin context (must own `tools`).
|
|
120
|
+
* @param control - the plugin's configuration surface.
|
|
121
|
+
* @param engine - the Cline Pass engine.
|
|
122
|
+
* @param store - the observation store.
|
|
123
|
+
*/
|
|
124
|
+
export function registerTools(ctx, { control, engine, store }) {
|
|
125
|
+
const readConfig = () => control.readConfig()
|
|
126
|
+
const pinOf = (model) => readConfig().perModel?.[model] ?? {}
|
|
127
|
+
|
|
128
|
+
// ── cline_pass_status ─────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
ctx.tools.register(defineTool({
|
|
131
|
+
name: 'cline_pass_status',
|
|
132
|
+
description: 'Report the Cline Pass provider state: route, accounts and whether each key is stored, account mode, model and pin counts. Read-only and free. Check this first when a Cline Pass model fails.',
|
|
133
|
+
parameters: {},
|
|
134
|
+
isConcurrencySafe: () => true,
|
|
135
|
+
timeoutMs: 30000,
|
|
136
|
+
output: {
|
|
137
|
+
schema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
additionalProperties: false,
|
|
140
|
+
properties: {
|
|
141
|
+
provider: { type: 'string', required: true },
|
|
142
|
+
displayName: { type: 'string', required: true },
|
|
143
|
+
baseURL: { type: 'string', required: true },
|
|
144
|
+
routeRegistered: { type: 'boolean', required: true },
|
|
145
|
+
settingsAvailable: { type: 'boolean', required: true },
|
|
146
|
+
accountMode: { type: 'string', required: true },
|
|
147
|
+
activeAccount: { type: 'string', required: true },
|
|
148
|
+
accounts: { type: 'array', required: true, items: ACCOUNT_SCHEMA },
|
|
149
|
+
knownModels: { type: 'integer', required: true },
|
|
150
|
+
pinnedModels: { type: 'integer', required: true },
|
|
151
|
+
probedModels: { type: 'integer', required: true },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
render: (_args, value) => {
|
|
155
|
+
const rows = value.accounts.map((account) => ` ${account.key}${account.enabled ? '' : ' (disabled)'} key=${account.keyConfigured ? account.keyHint : 'NOT SET'} ref=${account.apiKeyEnv}`)
|
|
156
|
+
return text([
|
|
157
|
+
`${value.displayName} provider "${value.provider}" @ ${value.baseURL}`,
|
|
158
|
+
`route registered=${value.routeRegistered} settings=${value.settingsAvailable ? 'available' : 'UNAVAILABLE (configuration cannot persist)'}`,
|
|
159
|
+
`accountMode=${value.accountMode} active=${value.activeAccount || '(first enabled)'}`,
|
|
160
|
+
`accounts (${value.accounts.length}):`,
|
|
161
|
+
...rows,
|
|
162
|
+
`models: known=${value.knownModels} pinned=${value.pinnedModels} probed=${value.probedModels}`,
|
|
163
|
+
].join('\n'))
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
async execute() {
|
|
167
|
+
const config = readConfig()
|
|
168
|
+
const accounts = await control.accountsWithKeys()
|
|
169
|
+
return {
|
|
170
|
+
provider: control.providerName,
|
|
171
|
+
displayName: control.displayName,
|
|
172
|
+
baseURL: config.baseURL,
|
|
173
|
+
routeRegistered: control.routeRegistered(),
|
|
174
|
+
settingsAvailable: control.settingsAvailable(),
|
|
175
|
+
accountMode: config.accountMode === 'roundrobin' ? 'roundrobin' : 'single',
|
|
176
|
+
activeAccount: String(config.activeAccount ?? ''),
|
|
177
|
+
accounts,
|
|
178
|
+
knownModels: (config.knownModels ?? []).length,
|
|
179
|
+
pinnedModels: Object.values(config.perModel ?? {}).filter((pin) => Array.isArray(pin?.upstreams) && pin.upstreams.length > 0).length,
|
|
180
|
+
probedModels: store.allMeta().filter((meta) => meta.pipeline !== undefined && meta.pipeline !== null).length,
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
}))
|
|
184
|
+
|
|
185
|
+
// ── cline_pass_models ─────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
ctx.tools.register(defineTool({
|
|
188
|
+
name: 'cline_pass_models',
|
|
189
|
+
description: 'List the Cline Pass models with their pipeline, discovered channels, per-channel verdicts and stored pin. Pass `model` to filter by substring, `refresh: true` to rescan the official model list. Free unless `refresh` is set.',
|
|
190
|
+
parameters: {
|
|
191
|
+
model: { type: 'string', description: 'Only return models whose id contains this text, e.g. "glm-5.2".' },
|
|
192
|
+
refresh: { type: 'boolean', description: 'Rescan the official model list first and adopt new ids.' },
|
|
193
|
+
},
|
|
194
|
+
isConcurrencySafe: () => true,
|
|
195
|
+
timeoutMs: 60000,
|
|
196
|
+
output: {
|
|
197
|
+
schema: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
additionalProperties: false,
|
|
200
|
+
properties: {
|
|
201
|
+
models: { type: 'array', required: true, items: MODEL_SCHEMA },
|
|
202
|
+
catalogCount: { type: 'integer', required: true },
|
|
203
|
+
catalogSources: { type: 'array', required: true, items: { type: 'string' } },
|
|
204
|
+
catalogFetchedAt: { type: 'integer', required: true },
|
|
205
|
+
added: { type: 'array', required: true, items: { type: 'string' } },
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
render: (_args, value) => {
|
|
209
|
+
const blocks = value.models.map((model) => {
|
|
210
|
+
const lines = [
|
|
211
|
+
`${model.id} pipeline=${model.pipeline || 'unknown'}${model.pinnable ? '' : ' (not pinnable)'}`,
|
|
212
|
+
` pinned=${model.pinned.length === 0 ? '(auto)' : model.pinned.join(' > ')} mode=${model.pinMode}${model.sort === '' ? '' : ` sort=${model.sort}`}${model.excluded.length === 0 ? '' : ` excluded=${model.excluded.join(',')}`}`,
|
|
213
|
+
]
|
|
214
|
+
if (model.upstreams.length > 0) {
|
|
215
|
+
const verdicts = new Map(model.upstreamStatus.map((entry) => [entry.upstream, entry.status]))
|
|
216
|
+
lines.push(` upstreams: ${model.upstreams.map((upstream) => `${upstream}[${statusLabel(verdicts.get(upstream) ?? 'unknown')}]`).join(' ')}`)
|
|
217
|
+
} else {
|
|
218
|
+
lines.push(' upstreams: (unknown — run cline_pass_probe for this model)')
|
|
219
|
+
}
|
|
220
|
+
if (model.lastProvider !== '') lines.push(` last=${model.lastProvider} (${model.lastMs}ms)`)
|
|
221
|
+
return lines.join('\n')
|
|
222
|
+
})
|
|
223
|
+
const head = `${value.models.length} Cline Pass model(s); official catalog=${value.catalogCount}${value.catalogSources.length === 0 ? '' : ` via ${value.catalogSources.join(',')}`}`
|
|
224
|
+
const added = value.added.length === 0 ? [] : [`newly adopted: ${value.added.join(', ')}`]
|
|
225
|
+
return text([head, ...added, ...blocks].join('\n\n'))
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
async execute(args) {
|
|
229
|
+
const config = readConfig()
|
|
230
|
+
let added = []
|
|
231
|
+
let catalog = store.catalog()
|
|
232
|
+
if (args.refresh === true) {
|
|
233
|
+
const scan = await control.refreshCatalog()
|
|
234
|
+
added = scan.added
|
|
235
|
+
catalog = store.catalog()
|
|
236
|
+
}
|
|
237
|
+
const needle = String(args.model ?? '').trim().toLowerCase()
|
|
238
|
+
const models = (config.knownModels ?? [])
|
|
239
|
+
.filter((id) => needle === '' || String(id).toLowerCase().includes(needle))
|
|
240
|
+
.map((id) => projectModel(id, pinOf(id), store.metaOf(id)))
|
|
241
|
+
return {
|
|
242
|
+
models,
|
|
243
|
+
catalogCount: catalog.ids?.length ?? 0,
|
|
244
|
+
catalogSources: (catalog.sources ?? []).map(String),
|
|
245
|
+
catalogFetchedAt: Number(catalog.fetchedAt ?? 0),
|
|
246
|
+
added,
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
}))
|
|
250
|
+
|
|
251
|
+
// ── cline_pass_probe ──────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
ctx.tools.register(defineTool({
|
|
254
|
+
name: 'cline_pass_probe',
|
|
255
|
+
description: 'Detect how one model is routed and harvest its pinnable upstream channels, with one tiny real request. Run it before pinning, and again when a pin that used to work stops working.',
|
|
256
|
+
parameters: {
|
|
257
|
+
model: { type: 'string', required: true, description: 'Model id, e.g. "cline-pass/glm-5.2".' },
|
|
258
|
+
},
|
|
259
|
+
timeoutMs: 600000,
|
|
260
|
+
output: {
|
|
261
|
+
schema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
additionalProperties: false,
|
|
264
|
+
properties: {
|
|
265
|
+
ok: { type: 'boolean', required: true },
|
|
266
|
+
model: { type: 'string', required: true },
|
|
267
|
+
error: { type: 'string', required: true },
|
|
268
|
+
ms: { type: 'integer', required: true },
|
|
269
|
+
pipeline: { type: 'string', required: true },
|
|
270
|
+
pinnable: { type: 'boolean', required: true },
|
|
271
|
+
canonicalSlug: { type: 'string', required: true },
|
|
272
|
+
lastProvider: { type: 'string', required: true },
|
|
273
|
+
upstreams: { type: 'array', required: true, items: { type: 'string' } },
|
|
274
|
+
availableProviders: { type: 'array', required: true, items: { type: 'string' } },
|
|
275
|
+
tier0: { type: 'array', required: true, items: { type: 'string' } },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
render: (_args, value) => {
|
|
279
|
+
if (!value.ok) return text(`probe ${value.model} failed: ${value.error}`)
|
|
280
|
+
return text([
|
|
281
|
+
`probe ${value.model}: ok in ${value.ms}ms`,
|
|
282
|
+
`pipeline=${value.pipeline} pinnable=${value.pinnable} canonicalSlug=${value.canonicalSlug || '-'} lastProvider=${value.lastProvider || '-'}`,
|
|
283
|
+
value.upstreams.length === 0 ? 'upstreams: (none discovered)' : `upstreams (${value.upstreams.length}): ${value.upstreams.join(', ')}`,
|
|
284
|
+
value.tier0.length === 0 ? '' : `tier0: ${value.tier0.join(', ')}`,
|
|
285
|
+
].filter((line) => line !== '').join('\n'))
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
async execute(args, exec) {
|
|
289
|
+
const model = String(args.model).trim()
|
|
290
|
+
const result = await engine.probe(model, { signal: exec.signal })
|
|
291
|
+
if (result.ok !== true) {
|
|
292
|
+
return { ok: false, model, error: firstLine(result.error, 400), ms: Number(result.ms ?? 0), pipeline: '', pinnable: false, canonicalSlug: '', lastProvider: '', upstreams: [], availableProviders: [], tier0: [] }
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
ok: true,
|
|
296
|
+
model,
|
|
297
|
+
error: '',
|
|
298
|
+
ms: Number(result.ms ?? 0),
|
|
299
|
+
pipeline: String(result.pipeline ?? ''),
|
|
300
|
+
pinnable: result.pinnable === true,
|
|
301
|
+
canonicalSlug: String(result.canonicalSlug ?? ''),
|
|
302
|
+
lastProvider: String(result.lastProvider ?? ''),
|
|
303
|
+
upstreams: (result.upstreams ?? []).map(String),
|
|
304
|
+
availableProviders: (result.availableProviders ?? []).map(String),
|
|
305
|
+
tier0: (result.tier0 ?? []).map(String),
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
}))
|
|
309
|
+
|
|
310
|
+
// ── cline_pass_validate ───────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
ctx.tools.register(defineTool({
|
|
313
|
+
name: 'cline_pass_validate',
|
|
314
|
+
description: 'Test every known channel of one model, one tiny real request each, and report a verdict per channel: available, rate-limited, not-pinnable, or auth-failed. This is the ground truth behind a pin. Probe the model first so its channel list exists.',
|
|
315
|
+
parameters: {
|
|
316
|
+
model: { type: 'string', required: true, description: 'Model id, e.g. "cline-pass/glm-5.2".' },
|
|
317
|
+
},
|
|
318
|
+
timeoutMs: 1800000,
|
|
319
|
+
output: {
|
|
320
|
+
schema: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
additionalProperties: false,
|
|
323
|
+
properties: {
|
|
324
|
+
ok: { type: 'boolean', required: true },
|
|
325
|
+
model: { type: 'string', required: true },
|
|
326
|
+
error: { type: 'string', required: true },
|
|
327
|
+
summary: {
|
|
328
|
+
type: 'object',
|
|
329
|
+
required: true,
|
|
330
|
+
additionalProperties: false,
|
|
331
|
+
properties: {
|
|
332
|
+
ok: { type: 'integer', required: true },
|
|
333
|
+
limited: { type: 'integer', required: true },
|
|
334
|
+
bad: { type: 'integer', required: true },
|
|
335
|
+
auth: { type: 'integer', required: true },
|
|
336
|
+
unknown: { type: 'integer', required: true },
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
upstreams: { type: 'array', required: true, items: { type: 'string' } },
|
|
340
|
+
results: {
|
|
341
|
+
type: 'array',
|
|
342
|
+
required: true,
|
|
343
|
+
items: {
|
|
344
|
+
type: 'object',
|
|
345
|
+
additionalProperties: false,
|
|
346
|
+
properties: {
|
|
347
|
+
upstream: { type: 'string', required: true },
|
|
348
|
+
status: { type: 'string', required: true },
|
|
349
|
+
ms: { type: 'integer', required: true },
|
|
350
|
+
note: { type: 'string', required: true },
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
render: (_args, value) => {
|
|
357
|
+
if (!value.ok) return text(`validate ${value.model} failed: ${value.error}`)
|
|
358
|
+
const rows = [...value.results]
|
|
359
|
+
.sort((a, b) => Number(a.status !== 'ok') - Number(b.status !== 'ok'))
|
|
360
|
+
.map((entry) => `${entry.upstream}: ${statusLabel(entry.status)} (${entry.ms}ms)${entry.note === '' ? '' : ` — ${entry.note}`}`)
|
|
361
|
+
return text([
|
|
362
|
+
`validate ${value.model}: ${value.results.length} upstream(s)`,
|
|
363
|
+
`available=${value.summary.ok} rate-limited=${value.summary.limited} not-pinnable=${value.summary.bad} auth-failed=${value.summary.auth} unknown=${value.summary.unknown}`,
|
|
364
|
+
...rows,
|
|
365
|
+
].join('\n'))
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
async execute(args, exec) {
|
|
369
|
+
const model = String(args.model).trim()
|
|
370
|
+
const meta = store.metaOf(model)
|
|
371
|
+
if (!Array.isArray(meta.upstreams) || meta.upstreams.length === 0) {
|
|
372
|
+
return {
|
|
373
|
+
ok: false,
|
|
374
|
+
model,
|
|
375
|
+
error: 'no known upstream channels: run cline_pass_probe for this model first',
|
|
376
|
+
summary: { ok: 0, limited: 0, bad: 0, auth: 0, unknown: 0 },
|
|
377
|
+
upstreams: [],
|
|
378
|
+
results: [],
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const outcome = await engine.validate(model, { signal: exec.signal })
|
|
382
|
+
return {
|
|
383
|
+
ok: true,
|
|
384
|
+
model,
|
|
385
|
+
error: '',
|
|
386
|
+
summary: outcome.summary,
|
|
387
|
+
upstreams: (store.metaOf(model).upstreams ?? []).map(String),
|
|
388
|
+
results: outcome.results.map((result) => ({
|
|
389
|
+
upstream: String(result.upstream),
|
|
390
|
+
status: String(result.status),
|
|
391
|
+
ms: Number(result.ms ?? 0),
|
|
392
|
+
note: firstLine(result.note, 160),
|
|
393
|
+
})),
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
}))
|
|
397
|
+
|
|
398
|
+
// ── cline_pass_test ───────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
ctx.tools.register(defineTool({
|
|
401
|
+
name: 'cline_pass_test',
|
|
402
|
+
description: 'Send one small real request through a pin and report which upstream served it, with the failover trace. `upstreams` and `exclude` override the stored pin for this call only; omit both to exercise it as stored. Nothing is persisted.',
|
|
403
|
+
parameters: {
|
|
404
|
+
model: { type: 'string', required: true, description: 'Model id, e.g. "cline-pass/glm-5.2".' },
|
|
405
|
+
upstreams: { type: 'array', items: { type: 'string' }, description: 'Ordered upstream candidates for this call only.' },
|
|
406
|
+
exclude: { type: 'array', items: { type: 'string' }, description: 'Upstream channels to exclude for this call only.' },
|
|
407
|
+
},
|
|
408
|
+
timeoutMs: 600000,
|
|
409
|
+
output: {
|
|
410
|
+
schema: {
|
|
411
|
+
type: 'object',
|
|
412
|
+
additionalProperties: false,
|
|
413
|
+
properties: {
|
|
414
|
+
ok: { type: 'boolean', required: true },
|
|
415
|
+
model: { type: 'string', required: true },
|
|
416
|
+
error: { type: 'string', required: true },
|
|
417
|
+
ms: { type: 'integer', required: true },
|
|
418
|
+
targets: { type: 'array', required: true, items: { type: 'string' } },
|
|
419
|
+
excluded: { type: 'array', required: true, items: { type: 'string' } },
|
|
420
|
+
actual: { type: 'string', required: true },
|
|
421
|
+
actualName: { type: 'string', required: true },
|
|
422
|
+
pipeline: { type: 'string', required: true },
|
|
423
|
+
pinnable: { type: 'boolean', required: true },
|
|
424
|
+
canonicalSlug: { type: 'string', required: true },
|
|
425
|
+
account: { type: 'string', required: true },
|
|
426
|
+
content: { type: 'string', required: true },
|
|
427
|
+
trace: { type: 'array', required: true, items: TRACE_SCHEMA },
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
render: (_args, value) => {
|
|
431
|
+
const trace = value.trace.map((entry) => ` ${entry.upstream} -> HTTP ${entry.status} in ${entry.ms}ms${entry.note === '' ? '' : ` — ${entry.note}`}`)
|
|
432
|
+
const head = value.ok
|
|
433
|
+
? `test ${value.model}: ok in ${value.ms}ms via ${value.actual || value.actualName || '(unknown)'}`
|
|
434
|
+
: `test ${value.model}: FAILED — ${value.error}`
|
|
435
|
+
return text([
|
|
436
|
+
head,
|
|
437
|
+
value.targets.length === 0 ? '' : `pinned: ${value.targets.join(' > ')}`,
|
|
438
|
+
value.excluded.length === 0 ? '' : `excluded: ${value.excluded.join(', ')}`,
|
|
439
|
+
value.ok ? `pipeline=${value.pipeline} canonicalSlug=${value.canonicalSlug || '-'} account=${value.account || '-'}` : '',
|
|
440
|
+
trace.length === 0 ? '' : `attempts (${trace.length}):`,
|
|
441
|
+
...trace,
|
|
442
|
+
value.content === '' ? '' : `reply: ${value.content}`,
|
|
443
|
+
].filter((line) => line !== '').join('\n'))
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
async execute(args, exec) {
|
|
447
|
+
const model = String(args.model).trim()
|
|
448
|
+
const result = await engine.test(model, {
|
|
449
|
+
upstreams: Array.isArray(args.upstreams) ? args.upstreams.map(String) : undefined,
|
|
450
|
+
exclude: Array.isArray(args.exclude) ? args.exclude.map(String) : undefined,
|
|
451
|
+
signal: exec.signal,
|
|
452
|
+
})
|
|
453
|
+
return {
|
|
454
|
+
ok: result.ok === true,
|
|
455
|
+
model,
|
|
456
|
+
error: firstLine(result.error, 400),
|
|
457
|
+
ms: Number(result.ms ?? 0),
|
|
458
|
+
targets: (result.targets ?? []).map(String),
|
|
459
|
+
excluded: (result.excluded ?? []).map(String),
|
|
460
|
+
actual: String(result.actual ?? ''),
|
|
461
|
+
actualName: String(result.actualName ?? ''),
|
|
462
|
+
pipeline: String(result.pipeline ?? ''),
|
|
463
|
+
pinnable: result.pinnable === true,
|
|
464
|
+
canonicalSlug: String(result.canonicalSlug ?? ''),
|
|
465
|
+
account: String(result.account ?? ''),
|
|
466
|
+
content: String(result.content ?? ''),
|
|
467
|
+
trace: (result.trace ?? []).map((entry) => ({
|
|
468
|
+
upstream: String(entry.upstream ?? '(auto)'),
|
|
469
|
+
status: Number(entry.status ?? 0),
|
|
470
|
+
ms: Number(entry.ms ?? 0),
|
|
471
|
+
note: firstLine(entry.note, 160),
|
|
472
|
+
})),
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
}))
|
|
476
|
+
|
|
477
|
+
// ── cline_pass_pin ────────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
ctx.tools.register(defineTool({
|
|
480
|
+
name: 'cline_pass_pin',
|
|
481
|
+
description: 'Persist one model\'s upstream pin. `upstreams` is the ordered candidate list, primary first (an empty array returns the model to automatic routing); `exclude` is never used; `pinMode` is "strict" or "preferred"; `sort` orders candidates by "cost", "ttft" or "tps" ("none" clears it). Only the fields you supply change.',
|
|
482
|
+
parameters: {
|
|
483
|
+
model: { type: 'string', required: true, description: 'Model id, e.g. "cline-pass/glm-5.2".' },
|
|
484
|
+
upstreams: { type: 'array', items: { type: 'string' }, description: 'Ordered pin list; an empty array clears pinning.' },
|
|
485
|
+
exclude: { type: 'array', items: { type: 'string' }, description: 'Channels that must never be used.' },
|
|
486
|
+
pinMode: { type: 'string', enum: ['strict', 'preferred'], description: 'How strictly to honor the pin list.' },
|
|
487
|
+
sort: { type: 'string', enum: ['cost', 'ttft', 'tps', 'none'], description: 'Reorder candidates by this metric; "none" clears it.' },
|
|
488
|
+
},
|
|
489
|
+
timeoutMs: 30000,
|
|
490
|
+
output: {
|
|
491
|
+
schema: {
|
|
492
|
+
type: 'object',
|
|
493
|
+
additionalProperties: false,
|
|
494
|
+
properties: {
|
|
495
|
+
ok: { type: 'boolean', required: true },
|
|
496
|
+
model: { type: 'string', required: true },
|
|
497
|
+
note: { type: 'string', required: true },
|
|
498
|
+
pinned: { type: 'array', required: true, items: { type: 'string' } },
|
|
499
|
+
excluded: { type: 'array', required: true, items: { type: 'string' } },
|
|
500
|
+
pinMode: { type: 'string', required: true },
|
|
501
|
+
sort: { type: 'string', required: true },
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
render: (_args, value) => text([
|
|
505
|
+
`${value.ok ? 'pinned' : 'pin failed'} ${value.model}`,
|
|
506
|
+
`upstreams: ${value.pinned.length === 0 ? '(auto)' : value.pinned.join(' > ')}`,
|
|
507
|
+
`exclude: ${value.excluded.length === 0 ? '(none)' : value.excluded.join(', ')}`,
|
|
508
|
+
`pinMode=${value.pinMode} sort=${value.sort === '' ? 'none' : value.sort}`,
|
|
509
|
+
value.note === '' ? '' : `note: ${value.note}`,
|
|
510
|
+
].filter((line) => line !== '').join('\n')),
|
|
511
|
+
},
|
|
512
|
+
async execute(args) {
|
|
513
|
+
const model = String(args.model).trim()
|
|
514
|
+
const config = readConfig()
|
|
515
|
+
const current = pinOf(model)
|
|
516
|
+
const next = {
|
|
517
|
+
upstreams: Array.isArray(args.upstreams) ? args.upstreams.map(String) : (current.upstreams ?? []).map(String),
|
|
518
|
+
exclude: Array.isArray(args.exclude) ? args.exclude.map(String) : (current.exclude ?? []).map(String),
|
|
519
|
+
pinMode: args.pinMode !== undefined ? args.pinMode : (current.pinMode ?? 'strict'),
|
|
520
|
+
sort: args.sort === undefined ? (current.sort ?? '') : (args.sort === 'none' ? '' : String(args.sort)),
|
|
521
|
+
}
|
|
522
|
+
await control.updateConfig({ perModel: { ...(config.perModel ?? {}), [model]: next } })
|
|
523
|
+
const stored = pinOf(model)
|
|
524
|
+
return {
|
|
525
|
+
ok: true,
|
|
526
|
+
model,
|
|
527
|
+
note: control.settingsAvailable()
|
|
528
|
+
? 'Saved to the provider configuration; the next model call uses it. Run cline_pass_test to confirm the gateway honors it.'
|
|
529
|
+
: 'Applied in memory only: the dsh settings service is unavailable, so this will not survive a restart.',
|
|
530
|
+
pinned: (stored.upstreams ?? []).map(String),
|
|
531
|
+
excluded: (stored.exclude ?? []).map(String),
|
|
532
|
+
pinMode: stored.pinMode === 'preferred' ? 'preferred' : 'strict',
|
|
533
|
+
sort: String(stored.sort ?? ''),
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
}))
|
|
537
|
+
|
|
538
|
+
// ── cline_pass_accounts ───────────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
ctx.tools.register(defineTool({
|
|
541
|
+
name: 'cline_pass_accounts',
|
|
542
|
+
description: 'Manage the Cline Pass account pool: list, add, remove, switch single/round-robin, or test a key with one real request. Keys go to the dsh credential store and are only ever shown masked.',
|
|
543
|
+
parameters: {
|
|
544
|
+
action: { type: 'string', required: true, enum: ['list', 'add', 'remove', 'mode', 'set', 'test'], description: 'Operation to perform.' },
|
|
545
|
+
name: { type: 'string', description: 'Account key (letters, digits, dot, dash, underscore).' },
|
|
546
|
+
key: { type: 'string', description: 'API key (sk_…), for add/test.' },
|
|
547
|
+
apiKeyEnv: { type: 'string', description: 'Credential name holding this account key.' },
|
|
548
|
+
displayName: { type: 'string', description: 'Human-readable label.' },
|
|
549
|
+
enabled: { type: 'boolean', description: 'Whether the account takes part in routing.' },
|
|
550
|
+
mode: { type: 'string', enum: ['single', 'roundrobin'], description: 'Pool mode.' },
|
|
551
|
+
active: { type: 'string', description: 'Account key used in single mode.' },
|
|
552
|
+
accounts: {
|
|
553
|
+
type: 'array',
|
|
554
|
+
description: 'Full replacement pool, for set.',
|
|
555
|
+
items: {
|
|
556
|
+
type: 'object',
|
|
557
|
+
additionalProperties: false,
|
|
558
|
+
properties: {
|
|
559
|
+
name: { type: 'string', required: true },
|
|
560
|
+
apiKeyEnv: { type: 'string' },
|
|
561
|
+
displayName: { type: 'string' },
|
|
562
|
+
enabled: { type: 'boolean' },
|
|
563
|
+
key: { type: 'string' },
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
timeoutMs: 180000,
|
|
569
|
+
output: {
|
|
570
|
+
schema: {
|
|
571
|
+
type: 'object',
|
|
572
|
+
additionalProperties: false,
|
|
573
|
+
properties: {
|
|
574
|
+
ok: { type: 'boolean', required: true },
|
|
575
|
+
action: { type: 'string', required: true },
|
|
576
|
+
note: { type: 'string', required: true },
|
|
577
|
+
accountMode: { type: 'string', required: true },
|
|
578
|
+
activeAccount: { type: 'string', required: true },
|
|
579
|
+
accounts: { type: 'array', required: true, items: ACCOUNT_SCHEMA },
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
render: (_args, value) => text([
|
|
583
|
+
`accounts.${value.action}: ${value.ok ? 'ok' : 'FAILED'}`,
|
|
584
|
+
value.note === '' ? '' : `note: ${value.note}`,
|
|
585
|
+
`accountMode=${value.accountMode} active=${value.activeAccount || '(first enabled)'} count=${value.accounts.length}`,
|
|
586
|
+
...value.accounts.map((account) => ` ${account.key}${account.enabled ? '' : ' (disabled)'} key=${account.keyConfigured ? account.keyHint : 'NOT SET'} ref=${account.apiKeyEnv}`),
|
|
587
|
+
].filter((line) => line !== '').join('\n')),
|
|
588
|
+
},
|
|
589
|
+
async execute(args, exec) {
|
|
590
|
+
const action = String(args.action)
|
|
591
|
+
let note = ''
|
|
592
|
+
const nameOf = (value) => String(value ?? '').trim()
|
|
593
|
+
if (action === 'test') {
|
|
594
|
+
let key = String(args.key ?? '').trim()
|
|
595
|
+
let label = args.name === undefined ? '(supplied key)' : nameOf(args.name)
|
|
596
|
+
let baseURL = readConfig().baseURL
|
|
597
|
+
if (key === '') {
|
|
598
|
+
if (args.name === undefined) throw new Error('cline_pass_accounts action="test" needs `key` or an existing `name`')
|
|
599
|
+
const account = control.accounts().find((entry) => entry.key === nameOf(args.name))
|
|
600
|
+
if (account === undefined) throw new Error(`no account named ${JSON.stringify(nameOf(args.name))}`)
|
|
601
|
+
key = await control.readCredential(account.apiKeyEnv)
|
|
602
|
+
if (key === '') throw new Error(`account ${JSON.stringify(account.key)} has no stored key at ${account.apiKeyEnv}; store one with action="add" key=… or through the dsh Models page`)
|
|
603
|
+
baseURL = account.baseURL || baseURL
|
|
604
|
+
}
|
|
605
|
+
const model = (readConfig().knownModels ?? [])[0] ?? 'cline-pass/glm-5.3-flash'
|
|
606
|
+
const result = await engine.testAccount({ key, baseURL, model, signal: exec.signal })
|
|
607
|
+
note = result.authorized
|
|
608
|
+
? `key for ${label} authorized in ${result.ms}ms`
|
|
609
|
+
: `key for ${label} FAILED: ${firstLine(result.error, 200)}`
|
|
610
|
+
} else if (action !== 'list') {
|
|
611
|
+
const config = readConfig()
|
|
612
|
+
const accounts = { ...(config.accounts ?? {}) }
|
|
613
|
+
if (action === 'set') {
|
|
614
|
+
if (!Array.isArray(args.accounts)) throw new Error('cline_pass_accounts action="set" needs `accounts`')
|
|
615
|
+
const next = {}
|
|
616
|
+
for (const [index, entry] of args.accounts.entries()) {
|
|
617
|
+
const key = nameOf(entry?.name) || `account-${index + 1}`
|
|
618
|
+
next[key] = {
|
|
619
|
+
displayName: String(entry?.displayName ?? key),
|
|
620
|
+
apiKeyEnv: String(entry?.apiKeyEnv ?? `CLINE_PASS_${key.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_KEY`),
|
|
621
|
+
enabled: entry?.enabled !== false,
|
|
622
|
+
baseURL: '',
|
|
623
|
+
}
|
|
624
|
+
if (typeof entry?.key === 'string' && entry.key.trim() !== '') {
|
|
625
|
+
await control.setCredential(next[key].apiKeyEnv, entry.key.trim())
|
|
626
|
+
note = `stored the key for ${key} in the credential store`
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
await control.updateConfig({ accounts: next })
|
|
630
|
+
} else if (action === 'add') {
|
|
631
|
+
const key = nameOf(args.name)
|
|
632
|
+
if (!/^[A-Za-z0-9._-]+$/.test(key)) throw new Error('cline_pass_accounts action="add" needs a `name` of letters, digits, dot, dash or underscore')
|
|
633
|
+
const config2 = readConfig()
|
|
634
|
+
// Adding the first account must not silently drop the implicit default
|
|
635
|
+
// account the top-level key fields describe: materialize it first.
|
|
636
|
+
if (Object.keys(accounts).length === 0) {
|
|
637
|
+
accounts.default = {
|
|
638
|
+
displayName: String(config2.displayName ?? 'default'),
|
|
639
|
+
apiKeyEnv: String(config2.apiKeyEnv ?? ''),
|
|
640
|
+
enabled: true,
|
|
641
|
+
baseURL: '',
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const apiKeyEnv = String(args.apiKeyEnv ?? `CLINE_PASS_${key.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_KEY`)
|
|
645
|
+
accounts[key] = {
|
|
646
|
+
displayName: String(args.displayName ?? key),
|
|
647
|
+
apiKeyEnv,
|
|
648
|
+
enabled: args.enabled !== false,
|
|
649
|
+
baseURL: '',
|
|
650
|
+
}
|
|
651
|
+
await control.updateConfig({ accounts })
|
|
652
|
+
if (typeof args.key === 'string' && args.key.trim() !== '') {
|
|
653
|
+
await control.setCredential(apiKeyEnv, args.key.trim())
|
|
654
|
+
note = `stored the key for ${key} at ${apiKeyEnv}`
|
|
655
|
+
} else {
|
|
656
|
+
note = `account ${key} added; store its key at ${apiKeyEnv} (pass key=… next time, or use the dsh Models page)`
|
|
657
|
+
}
|
|
658
|
+
} else if (action === 'remove') {
|
|
659
|
+
const key = nameOf(args.name)
|
|
660
|
+
if (accounts[key] === undefined) throw new Error(`no account named ${JSON.stringify(key)}`)
|
|
661
|
+
const removedRef = String(accounts[key]?.apiKeyEnv ?? config.apiKeyEnv ?? '')
|
|
662
|
+
delete accounts[key]
|
|
663
|
+
const active = readConfig().activeAccount === key ? '' : readConfig().activeAccount
|
|
664
|
+
await control.updateConfig({ accounts, activeAccount: active })
|
|
665
|
+
note = Object.keys(accounts).length === 0
|
|
666
|
+
? `removed ${key}; with no accounts left, requests fall back to the top-level ${readConfig().apiKeyEnv} credential`
|
|
667
|
+
: `removed ${key} (a value stored at ${removedRef} was left in place)`
|
|
668
|
+
} else if (action === 'mode') {
|
|
669
|
+
if (args.mode === undefined) throw new Error('cline_pass_accounts action="mode" needs `mode`')
|
|
670
|
+
}
|
|
671
|
+
if (args.mode !== undefined) await control.updateConfig({ accountMode: args.mode === 'roundrobin' ? 'roundrobin' : 'single' })
|
|
672
|
+
if (args.active !== undefined) await control.updateConfig({ activeAccount: nameOf(args.active) })
|
|
673
|
+
}
|
|
674
|
+
const config = readConfig()
|
|
675
|
+
return {
|
|
676
|
+
ok: true,
|
|
677
|
+
action,
|
|
678
|
+
note,
|
|
679
|
+
accountMode: config.accountMode === 'roundrobin' ? 'roundrobin' : 'single',
|
|
680
|
+
activeAccount: String(config.activeAccount ?? ''),
|
|
681
|
+
accounts: await control.accountsWithKeys(),
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
}))
|
|
685
|
+
|
|
686
|
+
// ── cline_pass_history ────────────────────────────────────────────────────
|
|
687
|
+
|
|
688
|
+
ctx.tools.register(defineTool({
|
|
689
|
+
name: 'cline_pass_history',
|
|
690
|
+
description: 'Read the in-process request history (newest first): account, upstream that served each call, latency, error. Read-only and free. In memory only, so it starts empty after a restart.',
|
|
691
|
+
parameters: {
|
|
692
|
+
limit: { type: 'integer', description: 'How many rows to return (default 25, max 100).' },
|
|
693
|
+
model: { type: 'string', description: 'Only return rows whose model id contains this text.' },
|
|
694
|
+
},
|
|
695
|
+
isConcurrencySafe: () => true,
|
|
696
|
+
timeoutMs: 30000,
|
|
697
|
+
output: {
|
|
698
|
+
schema: {
|
|
699
|
+
type: 'object',
|
|
700
|
+
additionalProperties: false,
|
|
701
|
+
properties: {
|
|
702
|
+
total: { type: 'integer', required: true },
|
|
703
|
+
entries: {
|
|
704
|
+
type: 'array',
|
|
705
|
+
required: true,
|
|
706
|
+
items: {
|
|
707
|
+
type: 'object',
|
|
708
|
+
additionalProperties: false,
|
|
709
|
+
properties: {
|
|
710
|
+
ts: { type: 'integer', required: true },
|
|
711
|
+
model: { type: 'string', required: true },
|
|
712
|
+
provider: { type: 'string', required: true },
|
|
713
|
+
canonical: { type: 'string', required: true },
|
|
714
|
+
account: { type: 'string', required: true },
|
|
715
|
+
ms: { type: 'integer', required: true },
|
|
716
|
+
stream: { type: 'boolean', required: true },
|
|
717
|
+
error: { type: 'string', required: true },
|
|
718
|
+
attempts: { type: 'array', required: true, items: { type: 'string' } },
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
render: (_args, value) => {
|
|
725
|
+
if (value.entries.length === 0) return text('No Cline Pass model calls recorded yet in this process.')
|
|
726
|
+
return text([
|
|
727
|
+
`last ${value.entries.length} of ${value.total} call(s):`,
|
|
728
|
+
...value.entries.map((entry) => {
|
|
729
|
+
const when = entry.ts === 0 ? '-' : new Date(entry.ts).toISOString()
|
|
730
|
+
const attempts = entry.attempts.length === 0 ? '' : ` attempts=${entry.attempts.join('>')}`
|
|
731
|
+
const error = entry.error === '' ? '' : ` error=${entry.error}`
|
|
732
|
+
return `${when} ${entry.model} -> ${entry.provider || '(auto)'}${entry.canonical === '' ? '' : ` [${entry.canonical}]`} ${entry.ms}ms${entry.stream ? ' stream' : ''} account=${entry.account || '-'}${attempts}${error}`
|
|
733
|
+
}),
|
|
734
|
+
].join('\n'))
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
async execute(args) {
|
|
738
|
+
const limit = Number.isSafeInteger(args.limit) && args.limit > 0 ? Math.min(args.limit, 100) : 25
|
|
739
|
+
const entries = store.readHistory(limit, args.model ?? '')
|
|
740
|
+
return {
|
|
741
|
+
total: store.historySize(),
|
|
742
|
+
entries: entries.map((entry) => ({
|
|
743
|
+
ts: Number(entry.ts ?? 0),
|
|
744
|
+
model: String(entry.model ?? ''),
|
|
745
|
+
provider: String(entry.provider ?? ''),
|
|
746
|
+
canonical: String(entry.canonical ?? ''),
|
|
747
|
+
account: String(entry.account ?? ''),
|
|
748
|
+
ms: Number(entry.ms ?? 0),
|
|
749
|
+
stream: entry.stream === true,
|
|
750
|
+
error: firstLine(entry.error, 200),
|
|
751
|
+
attempts: Array.isArray(entry.attempts) ? entry.attempts.map((item) => String(item ?? '')) : [],
|
|
752
|
+
})),
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
}))
|
|
756
|
+
}
|