@goodandready/dsh-clinebot 0.5.20 → 0.5.22

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/models.js CHANGED
@@ -52,12 +52,26 @@ export function getOriginalModelContext(nameOrId) {
52
52
  if (norm.includes('qwen')) return 1000000
53
53
  if (norm.includes('minimax')) return 1000000
54
54
  if (norm.includes('mimo')) return 1000000
55
+ if (norm.includes('glm-5.3') || norm.includes('glm-53')) return 1000000
56
+ if (norm.includes('deepseek-v4.1') || norm.includes('deepseek-v41')) return 1000000
55
57
  if (norm.includes('deepseek')) return 128000
56
58
  if (norm.includes('glm')) return 128000
57
59
  return 200000
58
60
  }
59
61
 
60
- function getOriginalModelProvider(nameOrId) {
62
+ export function getOriginalModelMaxTokens(nameOrId) {
63
+ const norm = String(nameOrId || '').toLowerCase()
64
+ if (norm.includes('qwen')) return 131072
65
+ if (norm.includes('glm')) return 128000
66
+ if (norm.includes('minimax')) return 128000
67
+ if (norm.includes('kimi')) return 65536
68
+ if (norm.includes('deepseek-v4.1') || norm.includes('deepseek-v41')) return 65536
69
+ if (norm.includes('deepseek')) return 16384
70
+ if (norm.includes('mimo')) return 16384
71
+ return 16384
72
+ }
73
+
74
+ export function getOriginalModelProvider(nameOrId) {
61
75
  const norm = String(nameOrId || '').toLowerCase()
62
76
  if (norm.includes('kimi')) return 'Moonshot AI'
63
77
  if (norm.includes('qwen')) return 'Alibaba Cloud'
@@ -68,181 +82,102 @@ function getOriginalModelProvider(nameOrId) {
68
82
  return 'Original Vendor'
69
83
  }
70
84
 
85
+ function createCuratedModel({
86
+ id, name, description,
87
+ ctx = 128000, maxTokens = 8192,
88
+ origCtx, origMax, origProv,
89
+ input = ['text'], category = 'general',
90
+ recommended = false, reasoningEfforts,
91
+ }) {
92
+ const c = origCtx || getOriginalModelContext(id)
93
+ const p = origProv || getOriginalModelProvider(id)
94
+ const om = origMax || getOriginalModelMaxTokens(id)
95
+ return {
96
+ id, name, description,
97
+ clineContextLength: ctx, defaultContextLength: ctx, contextLength: ctx,
98
+ originalContextLength: c, originalProvider: p, originalMaxTokens: om,
99
+ maxTokens, defaultMaxTokens: maxTokens,
100
+ input, category, recommended, isCustom: false,
101
+ ...(reasoningEfforts ? { reasoningEfforts } : {}),
102
+ }
103
+ }
104
+
71
105
  export const CLINE_MODELS = Object.freeze([
72
- {
106
+ createCuratedModel({
73
107
  id: 'cline-pass/deepseek-v4-flash',
74
108
  name: 'DeepSeek V4 Flash',
75
109
  description: 'High-speed reasoning & code completion model optimized for agentic loops.',
76
- clineContextLength: 128000,
77
- defaultContextLength: 128000,
78
- originalContextLength: 128000,
79
- originalProvider: 'DeepSeek',
80
- contextLength: 128000,
81
- maxTokens: 8192,
82
- input: ['text', 'image'],
83
- category: 'coding',
84
- recommended: true,
85
- isCustom: false,
110
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: true,
86
111
  reasoningEfforts: ['low', 'medium', 'high'],
87
- },
88
- {
112
+ }),
113
+ createCuratedModel({
89
114
  id: 'cline-pass/deepseek-v4-pro',
90
115
  name: 'DeepSeek V4 Pro',
91
116
  description: 'Flagship reasoning and multi-turn architectural coding model.',
92
- clineContextLength: 128000,
93
- defaultContextLength: 128000,
94
- originalContextLength: 128000,
95
- originalProvider: 'DeepSeek',
96
- contextLength: 128000,
97
- maxTokens: 8192,
98
- input: ['text', 'image'],
99
- category: 'coding',
100
- recommended: true,
101
- isCustom: false,
117
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: true,
102
118
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
103
- },
104
- {
119
+ }),
120
+ createCuratedModel({
105
121
  id: 'cline-pass/glm-5.2',
106
122
  name: 'GLM 5.2',
107
123
  description: 'Bilingual general & coding model with strong instruction following.',
108
- clineContextLength: 128000,
109
- defaultContextLength: 128000,
110
- originalContextLength: 128000,
111
- originalProvider: 'Zhipu AI',
112
- contextLength: 128000,
113
- maxTokens: 8192,
114
- input: ['text'],
115
- category: 'general',
116
- recommended: false,
117
- isCustom: false,
124
+ ctx: 128000, maxTokens: 8192, input: ['text'], category: 'general', recommended: false,
118
125
  reasoningEfforts: ['low', 'medium', 'high'],
119
- },
120
- {
126
+ }),
127
+ createCuratedModel({
121
128
  id: 'cline-pass/kimi-k3',
122
129
  name: 'Kimi K3',
123
130
  description: 'Long-context reasoning & document synthesis model by Moonshot AI.',
124
- clineContextLength: 200000,
125
- defaultContextLength: 200000,
126
- originalContextLength: 2000000,
127
- originalProvider: 'Moonshot AI',
128
- contextLength: 200000,
129
- maxTokens: 8192,
130
- input: ['text'],
131
- category: 'reasoning',
132
- recommended: true,
133
- isCustom: false,
131
+ ctx: 200000, maxTokens: 8192, input: ['text'], category: 'reasoning', recommended: true,
134
132
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
135
- },
136
- {
133
+ }),
134
+ createCuratedModel({
137
135
  id: 'cline-pass/kimi-k2.7-code',
138
136
  name: 'Kimi K2.7 Code',
139
137
  description: 'Specialized coding model with agentic tool calling support.',
140
- clineContextLength: 200000,
141
- defaultContextLength: 200000,
142
- originalContextLength: 2000000,
143
- originalProvider: 'Moonshot AI',
144
- contextLength: 200000,
145
- maxTokens: 8192,
146
- input: ['text'],
147
- category: 'coding',
148
- recommended: false,
149
- isCustom: false,
150
- },
151
- {
138
+ ctx: 200000, maxTokens: 8192, input: ['text'], category: 'coding', recommended: false,
139
+ }),
140
+ createCuratedModel({
152
141
  id: 'cline-pass/kimi-k2.6',
153
142
  name: 'Kimi K2.6',
154
143
  description: 'Balanced long-context conversational model.',
155
- clineContextLength: 200000,
156
- defaultContextLength: 200000,
157
- originalContextLength: 2000000,
158
- originalProvider: 'Moonshot AI',
159
- contextLength: 200000,
160
- maxTokens: 8192,
161
- input: ['text'],
162
- category: 'general',
163
- recommended: false,
164
- isCustom: false,
165
- },
166
- {
144
+ ctx: 200000, maxTokens: 8192, input: ['text'], category: 'general', recommended: false,
145
+ }),
146
+ createCuratedModel({
167
147
  id: 'cline-pass/qwen3.7-max',
168
148
  name: 'Qwen 3.7 Max',
169
149
  description: 'Large-scale multimodal foundation model from Alibaba Cloud with reasoning support.',
170
- clineContextLength: 128000,
171
- defaultContextLength: 128000,
172
- originalContextLength: 1000000,
173
- originalProvider: 'Alibaba Cloud',
174
- contextLength: 128000,
175
- maxTokens: 8192,
176
- input: ['text', 'image'],
177
- category: 'multimodal',
178
- recommended: true,
179
- isCustom: false,
150
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'multimodal', recommended: true,
180
151
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
181
- },
182
- {
152
+ }),
153
+ createCuratedModel({
183
154
  id: 'cline-pass/qwen3.7-plus',
184
155
  name: 'Qwen 3.7 Plus',
185
156
  description: 'Fast, capable multimodal model with strong multilingual skills and reasoning.',
186
- clineContextLength: 128000,
187
- defaultContextLength: 128000,
188
- originalContextLength: 1000000,
189
- originalProvider: 'Alibaba Cloud',
190
- contextLength: 128000,
191
- maxTokens: 8192,
192
- input: ['text', 'image'],
193
- category: 'multimodal',
194
- recommended: false,
195
- isCustom: false,
157
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'multimodal', recommended: false,
196
158
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
197
- },
198
- {
159
+ }),
160
+ createCuratedModel({
199
161
  id: 'cline-pass/minimax-m3',
200
162
  name: 'MiniMax M3',
201
163
  description: 'High throughput reasoning and synthesis model.',
202
- clineContextLength: 200000,
203
- defaultContextLength: 200000,
204
- originalContextLength: 1000000,
205
- originalProvider: 'MiniMax',
206
- contextLength: 200000,
207
- maxTokens: 8192,
208
- input: ['text'],
209
- category: 'reasoning',
210
- recommended: false,
211
- isCustom: false,
164
+ ctx: 200000, maxTokens: 8192, input: ['text'], category: 'reasoning', recommended: false,
212
165
  reasoningEfforts: ['low', 'medium', 'high'],
213
- },
214
- {
166
+ }),
167
+ createCuratedModel({
215
168
  id: 'cline-pass/mimo-v2.5',
216
169
  name: 'MiMo V2.5',
217
170
  description: 'Xiaomi MiMo efficient instruction model with reasoning and multimodal support.',
218
- clineContextLength: 128000,
219
- defaultContextLength: 128000,
220
- originalContextLength: 1000000,
221
- originalProvider: 'Xiaomi',
222
- contextLength: 128000,
223
- maxTokens: 8192,
224
- input: ['text', 'image'],
225
- category: 'general',
226
- recommended: false,
227
- isCustom: false,
171
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'general', recommended: false,
228
172
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
229
- },
230
- {
173
+ }),
174
+ createCuratedModel({
231
175
  id: 'cline-pass/mimo-v2.5-pro',
232
176
  name: 'MiMo V2.5 Pro',
233
177
  description: 'Xiaomi MiMo advanced agentic reasoning model with multimodal support.',
234
- clineContextLength: 128000,
235
- defaultContextLength: 128000,
236
- originalContextLength: 1000000,
237
- originalProvider: 'Xiaomi',
238
- contextLength: 128000,
239
- maxTokens: 8192,
240
- input: ['text', 'image'],
241
- category: 'coding',
242
- recommended: false,
243
- isCustom: false,
178
+ ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: false,
244
179
  reasoningEfforts: ['low', 'medium', 'high', 'max'],
245
- },
180
+ }),
246
181
  ])
247
182
 
248
183
  /**
@@ -303,6 +238,7 @@ export function parsePlanIncludedModels(includedInput) {
303
238
  const isVision = /(vision|vl|multimodal|omni|image)/i.test(name)
304
239
  const hasReasoning = /(reason|think|r1|pro|flash|max|plus|k3|m3|glm|qwen|mimo)/i.test(name)
305
240
  const origCtx = getOriginalModelContext(name)
241
+ const origMax = getOriginalModelMaxTokens(name)
306
242
  const origProv = getOriginalModelProvider(name)
307
243
  matched.push({
308
244
  id: `cline-pass/${idPart}`,
@@ -311,9 +247,11 @@ export function parsePlanIncludedModels(includedInput) {
311
247
  clineContextLength: 200000,
312
248
  defaultContextLength: 200000,
313
249
  originalContextLength: origCtx,
250
+ originalMaxTokens: origMax,
314
251
  originalProvider: origProv,
315
252
  contextLength: 200000,
316
253
  maxTokens: 8192,
254
+ defaultMaxTokens: 8192,
317
255
  input: isVision ? ['text', 'image'] : ['text'],
318
256
  category: hasReasoning ? 'reasoning' : (isVision ? 'multimodal' : 'general'),
319
257
  recommended: false,
@@ -351,26 +289,31 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
351
289
  }
352
290
  }
353
291
 
292
+ const wildcardOv = overridesMap.get('*')
354
293
  function applyOverride(m) {
355
294
  const defaultCtx = m.clineContextLength || m.defaultContextLength || m.contextLength || 200000
295
+ const defaultMax = m.defaultMaxTokens || m.maxTokens || 8192
356
296
  const origCtx = m.originalContextLength || getOriginalModelContext(m.id)
297
+ const origMax = m.originalMaxTokens || getOriginalModelMaxTokens(m.id)
357
298
  const origProv = m.originalProvider || getOriginalModelProvider(m.id)
358
- const ov = overridesMap.get(m.id)
299
+ const ov = overridesMap.get(m.id) || wildcardOv
359
300
  const activeCtx = ov && typeof ov.contextLength === 'number' && ov.contextLength > 0
360
301
  ? ov.contextLength
361
302
  : defaultCtx
362
303
  const activeMaxTokens = ov && typeof ov.maxTokens === 'number' && ov.maxTokens > 0
363
304
  ? ov.maxTokens
364
- : m.maxTokens || 8192
305
+ : defaultMax
365
306
  return {
366
307
  ...m,
367
308
  contextLength: activeCtx,
368
309
  maxTokens: activeMaxTokens,
369
310
  defaultContextLength: defaultCtx,
370
311
  clineContextLength: defaultCtx,
312
+ defaultMaxTokens: defaultMax,
371
313
  originalContextLength: origCtx,
314
+ originalMaxTokens: origMax,
372
315
  originalProvider: origProv,
373
- isContextOverridden: activeCtx !== defaultCtx,
316
+ isContextOverridden: activeCtx !== defaultCtx || activeMaxTokens !== defaultMax,
374
317
  }
375
318
  }
376
319
 
@@ -392,7 +335,9 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
392
335
  clineContextLength: curated.clineContextLength || curated.contextLength,
393
336
  defaultContextLength: curated.defaultContextLength || curated.contextLength,
394
337
  originalContextLength: curated.originalContextLength || getOriginalModelContext(curated.id),
338
+ originalMaxTokens: curated.originalMaxTokens || getOriginalModelMaxTokens(curated.id),
395
339
  originalProvider: curated.originalProvider || getOriginalModelProvider(curated.id),
340
+ defaultMaxTokens: curated.defaultMaxTokens || curated.maxTokens || 8192,
396
341
  input: dm.input && dm.input.length ? dm.input : curated.input,
397
342
  category: dm.category || curated.category,
398
343
  reasoningEfforts: dm.reasoningEfforts !== undefined ? dm.reasoningEfforts : curated.reasoningEfforts,
@@ -404,7 +349,9 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
404
349
  clineContextLength: dm.contextLength || 200000,
405
350
  defaultContextLength: dm.contextLength || 200000,
406
351
  originalContextLength: dm.originalContextLength || getOriginalModelContext(dm.id),
352
+ originalMaxTokens: dm.originalMaxTokens || getOriginalModelMaxTokens(dm.id),
407
353
  originalProvider: dm.originalProvider || getOriginalModelProvider(dm.id),
354
+ defaultMaxTokens: dm.maxTokens || 8192,
408
355
  unverified: false,
409
356
  })
410
357
  })
@@ -239,6 +239,22 @@ export async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
239
239
  }
240
240
  }
241
241
 
242
+ let existingModels = []
243
+ try {
244
+ if (typeof settings.describe === 'function') {
245
+ const descriptors = settings.describe()
246
+ const piAiDesc = Array.isArray(descriptors) ? descriptors.find((d) => d?.ns === LLM_PI_AI_NS) : null
247
+ const existingProv = piAiDesc?.value?.providers?.[PROVIDER_ID]
248
+ if (Array.isArray(existingProv?.models)) existingModels = existingProv.models
249
+ } else if (typeof settings.get === 'function') {
250
+ const piAiVal = settings.get(LLM_PI_AI_NS)
251
+ const existingProv = piAiVal?.providers?.[PROVIDER_ID]
252
+ if (Array.isArray(existingProv?.models)) existingModels = existingProv.models
253
+ }
254
+ } catch {
255
+ /* ignore read error */
256
+ }
257
+
242
258
  const providerObj = buildPiAiProvider({
243
259
  baseUrl: effectiveBaseUrl,
244
260
  apiKeyEnv: isProxy ? LOCAL_PROXY_KEY_ENV : (activeAcc?.apiKeyEnv || pub.apiKeyEnv),
@@ -246,6 +262,7 @@ export async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
246
262
  dynamicModels: pub.dynamicModels,
247
263
  modelContextOverrides: pub.modelContextOverrides,
248
264
  customModels: pub.customModels,
265
+ existingModels,
249
266
  displayName: PROVIDER_DISPLAY_NAME,
250
267
  })
251
268
 
@@ -257,6 +274,10 @@ export async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
257
274
  },
258
275
  ])
259
276
 
277
+ ctx?.logger?.info?.(
278
+ `[dsh-clinebot] Synchronized llm-pi-ai provider "${PROVIDER_ID}" with ${providerObj.models.length} models.`
279
+ )
280
+
260
281
  return providerObj
261
282
  }
262
283
 
@@ -14,9 +14,9 @@ export const LOCAL_PROXY_KEY_ENV = 'CLINEBOT_LOCAL_PROXY_TOKEN'
14
14
  let cachedToken = null
15
15
 
16
16
  const require = createRequire(import.meta.url)
17
- let packageVersion = '0.5.20'
17
+ let packageVersion = '0.5.22'
18
18
  try {
19
- packageVersion = require('../package.json').version || '0.5.20'
19
+ packageVersion = require('../package.json').version || '0.5.22'
20
20
  } catch (err) {
21
21
  /* fallback to default version if manifest is unreadable */
22
22
  }
@@ -118,3 +118,23 @@ export function sendProxyTerminalError(res, { hadTimeout, is429Exhausted, lastEr
118
118
  },
119
119
  })
120
120
  }
121
+
122
+
123
+ /**
124
+ * Safely extract standard and reasoning token counts from usage object.
125
+ */
126
+ export function extractUsageMetrics(usage) {
127
+ if (!usage || typeof usage !== 'object') {
128
+ return { promptTokens: 0, completionTokens: 0, reasoningTokens: 0, totalTokens: 0 }
129
+ }
130
+ const promptTokens = Number(usage.prompt_tokens) || 0
131
+ const completionTokens = Number(usage.completion_tokens) || 0
132
+ const reasoningTokens = Number(
133
+ usage.completion_tokens_details?.reasoning_tokens ||
134
+ usage.reasoning_tokens ||
135
+ usage.prompt_tokens_details?.reasoning_tokens ||
136
+ 0
137
+ )
138
+ const totalTokens = Number(usage.total_tokens) || (promptTokens + completionTokens)
139
+ return { promptTokens, completionTokens, reasoningTokens, totalTokens }
140
+ }
@@ -2,8 +2,8 @@ import { writeJson, readBody } from '../http.js'
2
2
  import { assertTrustedSettingsRequest } from '../access.js'
3
3
  import { publicConfig, plainConfig, Config } from '../config.js'
4
4
  import { resolveActiveAccountKey, resolvePathWithHome } from '../provider-sync.js'
5
- import { getAllModels, saveModelsDiskCache, getOriginalModelContext } from '../models.js'
6
- import { fetchUsageLimits } from '../cline-client.js'
5
+ import { getAllModels, saveModelsDiskCache, getOriginalModelContext, getOriginalModelMaxTokens } from '../models.js'
6
+ import { fetchUsageLimits, smokeChat } from '../cline-client.js'
7
7
  import { publicUsage } from '../http.js'
8
8
 
9
9
  export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderState }) {
@@ -129,15 +129,36 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
129
129
  let currentOverrides = Array.isArray(pub.modelContextOverrides) ? [...pub.modelContextOverrides] : []
130
130
 
131
131
  if (body.mode === 'all-original') {
132
- // Set all models to their authentic original creator context limits
132
+ // Set all models to their authentic original creator context & token limits
133
133
  currentOverrides = allModels.map((m) => ({
134
134
  modelId: m.id,
135
135
  contextLength: m.originalContextLength || getOriginalModelContext(m.id),
136
- maxTokens: m.maxTokens || 8192,
136
+ maxTokens: m.originalMaxTokens || getOriginalModelMaxTokens(m.id),
137
137
  }))
138
138
  } else if (body.mode === 'all-default' || body.mode === 'reset-all') {
139
139
  // Reset all overrides back to provider defaults
140
140
  currentOverrides = []
141
+ } else if (body.mode === 'apply-all') {
142
+ // Bulk apply limits across all models and set wildcard default
143
+ const rawCtx = Number(body.contextLength)
144
+ const contextLength = Number.isFinite(rawCtx) && rawCtx > 0 ? Math.max(1000, rawCtx) : undefined
145
+ const rawMax = Number(body.maxTokens)
146
+ const maxTokens = Number.isFinite(rawMax) && rawMax > 0 ? Math.max(256, rawMax) : undefined
147
+
148
+ currentOverrides = allModels.map((m) => {
149
+ const existing = currentOverrides.find((o) => o.modelId === m.id)
150
+ return {
151
+ modelId: m.id,
152
+ contextLength: contextLength !== undefined ? contextLength : (existing?.contextLength || m.contextLength),
153
+ maxTokens: maxTokens !== undefined ? maxTokens : (existing?.maxTokens || m.maxTokens),
154
+ }
155
+ })
156
+ currentOverrides = currentOverrides.filter((o) => o.modelId !== '*')
157
+ currentOverrides.push({
158
+ modelId: '*',
159
+ contextLength: contextLength || 200000,
160
+ maxTokens: maxTokens || 16384,
161
+ })
141
162
  } else if (Array.isArray(body.overrides)) {
142
163
  // Bulk array override
143
164
  currentOverrides = body.overrides
@@ -153,11 +174,19 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
153
174
  currentOverrides = currentOverrides.filter((o) => o.modelId !== modelId)
154
175
  } else {
155
176
  const rawCtx = Number(body.contextLength)
156
- const contextLength = Number.isFinite(rawCtx) && rawCtx > 0 ? Math.max(1000, rawCtx) : 200000
157
177
  const rawMax = Number(body.maxTokens)
158
- const maxTokens = Number.isFinite(rawMax) && rawMax > 0 ? Math.max(256, rawMax) : 8192
159
-
160
178
  const existingIdx = currentOverrides.findIndex((o) => o.modelId === modelId)
179
+ const existingEntry = existingIdx >= 0 ? currentOverrides[existingIdx] : null
180
+ const targetModel = allModels.find((m) => m.id === modelId)
181
+
182
+ const contextLength = Number.isFinite(rawCtx) && rawCtx > 0
183
+ ? Math.max(1000, rawCtx)
184
+ : (existingEntry?.contextLength || targetModel?.contextLength || 200000)
185
+
186
+ const maxTokens = Number.isFinite(rawMax) && rawMax > 0
187
+ ? Math.max(256, rawMax)
188
+ : (existingEntry?.maxTokens || targetModel?.maxTokens || 8192)
189
+
161
190
  const newEntry = { modelId, contextLength, maxTokens }
162
191
  if (existingIdx >= 0) {
163
192
  currentOverrides[existingIdx] = newEntry
@@ -211,4 +240,38 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
211
240
  }
212
241
  },
213
242
  }), 'dsh-clinebot: /usage')
243
+
244
+ // POST /dsh-clinebot/models/probe — benchmark TTFT latency for specific model
245
+ ctx.effect(() => ctx.webServer.register({
246
+ kind: 'exact',
247
+ path: '/dsh-clinebot/models/probe',
248
+ handler: async (req, res) => {
249
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
250
+ if (!assertTrustedSettingsRequest(req, res)) return
251
+ try {
252
+ const bodyBuf = await readBody(req)
253
+ let body = {}
254
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
255
+ const pub = publicConfig(live())
256
+ const activeKey = await resolveActiveAccountKey(ctx, live())
257
+ if (!activeKey.value) {
258
+ return writeJson(res, 400, { ok: false, error: 'API key not found' })
259
+ }
260
+ const modelId = body.modelId || body.model || pub.defaultModel
261
+ const result = await smokeChat(pub.baseUrl, activeKey.value, {
262
+ model: modelId,
263
+ timeoutMs: Math.min(pub.smokeTimeoutMs || 25000, 15000),
264
+ })
265
+ return writeJson(res, 200, {
266
+ ok: result.ok,
267
+ modelId,
268
+ latencyMs: result.latencyMs,
269
+ status: result.status,
270
+ error: result.error || null,
271
+ })
272
+ } catch (err) {
273
+ return writeJson(res, 500, { ok: false, error: String(err?.message || err) })
274
+ }
275
+ },
276
+ }), 'dsh-clinebot: /models/probe')
214
277
  }
@@ -27,6 +27,7 @@ import {
27
27
  isAuthorizedProxyRequest,
28
28
  USER_AGENT,
29
29
  sendProxyTerminalError,
30
+ extractUsageMetrics,
30
31
  } from '../proxy-token.js'
31
32
 
32
33
  export { hasDisallowedBrowserOrigin, isAuthorizedProxyRequest, USER_AGENT }
@@ -120,7 +121,7 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
120
121
  return
121
122
  }
122
123
 
123
- const routeResult = resolveSessionAccount(sessionId, pool, pub.activeAccount, pub.accountMode)
124
+ const routeResult = resolveSessionAccount(sessionId, pool, pub.activeAccount, pub.accountMode, body?.model, pub.modelRoutes)
124
125
  const targetAccount = routeResult.account
125
126
  lastTargetAccount = targetAccount
126
127
 
@@ -297,6 +298,7 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
297
298
 
298
299
  let promptTokens = 0
299
300
  let completionTokens = 0
301
+ let reasoningTokens = 0
300
302
  let totalTokens = 0
301
303
  let buffer = ''
302
304
  let streamError = null
@@ -350,9 +352,11 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
350
352
  try {
351
353
  const parsed = JSON.parse(trimmed.slice(5).trim())
352
354
  if (parsed.usage) {
353
- promptTokens = parsed.usage.prompt_tokens || promptTokens
354
- completionTokens = parsed.usage.completion_tokens || completionTokens
355
- totalTokens = parsed.usage.total_tokens || totalTokens
355
+ const u = extractUsageMetrics(parsed.usage)
356
+ promptTokens = u.promptTokens || promptTokens
357
+ completionTokens = u.completionTokens || completionTokens
358
+ reasoningTokens = u.reasoningTokens || reasoningTokens
359
+ totalTokens = u.totalTokens || totalTokens
356
360
  }
357
361
  } catch {}
358
362
  }
@@ -419,6 +423,7 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
419
423
  accountId: targetAccount.id || targetAccount.label,
420
424
  promptTokens,
421
425
  completionTokens,
426
+ reasoningTokens,
422
427
  totalTokens: totalTokens || (promptTokens + completionTokens),
423
428
  isError: isStreamFailed,
424
429
  statsPath: pub.statsPath
@@ -495,12 +500,14 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
495
500
  }
496
501
 
497
502
  cleanupClientListeners()
503
+ const nonStreamUsage = extractUsageMetrics(resJson?.usage)
498
504
  recordUsage({
499
505
  model: body.model,
500
506
  accountId: targetAccount.id || targetAccount.label,
501
- promptTokens: resJson?.usage?.prompt_tokens || 0,
502
- completionTokens: resJson?.usage?.completion_tokens || 0,
503
- totalTokens: resJson?.usage?.total_tokens || 0,
507
+ promptTokens: nonStreamUsage.promptTokens,
508
+ completionTokens: nonStreamUsage.completionTokens,
509
+ reasoningTokens: nonStreamUsage.reasoningTokens,
510
+ totalTokens: nonStreamUsage.totalTokens,
504
511
  isError: false,
505
512
  statsPath: pub.statsPath
506
513
  })
@@ -582,4 +589,4 @@ export function registerProxyRoutes(ctx, routeEnv) {
582
589
  path: '/dsh-clinebot/v1/models',
583
590
  handler: modelsHandler,
584
591
  }), 'dsh-clinebot: /v1/models')
585
- }
592
+ }
@@ -3,7 +3,7 @@ import { assertTrustedSettingsRequest } from '../access.js'
3
3
  import { publicConfig, plainConfig, Config } from '../config.js'
4
4
  import { buildStatus } from '../provider-sync.js'
5
5
  import { saveCredentialKey, smokeChat, DEFAULT_API_KEY_ENV } from '../cline-client.js'
6
- import { getStatsSummary, resetStats } from '../stats-storage.js'
6
+ import { getStatsSummary, resetStats, exportStatsAsCsv } from '../stats-storage.js'
7
7
 
8
8
  export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProviderState, triggerAutoDiscover }) {
9
9
  // 1. GET /dsh-clinebot/status
@@ -212,4 +212,23 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
212
212
  }
213
213
  },
214
214
  }), 'dsh-clinebot: /stats/reset')
215
+
216
+ // 6. GET /dsh-clinebot/stats/export — export token telemetry as CSV
217
+ ctx.effect(() => ctx.webServer.register({
218
+ kind: 'exact',
219
+ path: '/dsh-clinebot/stats/export',
220
+ handler: async (req, res) => {
221
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
222
+ if (!assertTrustedSettingsRequest(req, res)) return
223
+ try {
224
+ const csv = exportStatsAsCsv()
225
+ res.statusCode = 200
226
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8')
227
+ res.setHeader('Content-Disposition', 'attachment; filename="clinebot-stats.csv"')
228
+ res.end(csv)
229
+ } catch (err) {
230
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
231
+ }
232
+ },
233
+ }), 'dsh-clinebot: /stats/export')
215
234
  }