@goodandready/dsh-clinebot 0.6.0 → 0.6.2

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.
@@ -1,578 +0,0 @@
1
- /**
2
- * Transparent Loopback Proxy & Zero-Downtime Failover (Issues #127, #145, #147, #211, #212)
3
- *
4
- * Exposes local OpenAI-compatible endpoints:
5
- * - POST /dsh-clinebot/v1/chat/completions
6
- * - GET /dsh-clinebot/v1/models
7
- *
8
- * Implements:
9
- * 1. Bearer local proxy token verification (Issue #145).
10
- * 2. Origin & Sec-Fetch-Site rejection to prevent cross-site/browser CSRF attacks (Issue #145).
11
- * 3. Dynamic User-Agent extraction from package.json (Issue #147).
12
- * 4. Configurable upstream timeout with multi-account failover and 504 handling (Issue #147).
13
- * 5. Sticky session routing with least-used and round-robin quota allocation (Issue #211).
14
- * 6. Pre-first-token failover: commits SSE headers only after first valid token/chunk (Issue #212).
15
- * 7. Streaming SSE token usage interception & persistent telemetry recording.
16
- */
17
-
18
- import { writeJson, readBody } from '../http.js'
19
- import { publicConfig, plainConfig } from '../config.js'
20
- import { resolveSessionAccount, markAccountCooldown } from '../session-router.js'
21
- import { recordUsage } from '../stats-storage.js'
22
- import { getAllModels, DEFAULT_MODEL_ID } from '../models.js'
23
- import { optimizePromptCacheAndBudget } from '../multimodal-budget.js'
24
- import {
25
- getLocalProxyToken, hasDisallowedBrowserOrigin, isAuthorizedProxyRequest,
26
- assertAuthorizedProxyRequest,
27
- USER_AGENT, sendProxyTerminalError, extractUsageMetrics
28
- } from '../proxy-token.js'
29
-
30
- export { hasDisallowedBrowserOrigin, isAuthorizedProxyRequest, assertAuthorizedProxyRequest, USER_AGENT }
31
-
32
- export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncProviderState }) {
33
- return async (req, res) => {
34
- if (req.method !== 'POST') {
35
- return writeJson(res, 405, { ok: false, error: 'POST only' })
36
- }
37
-
38
- const auth = await assertAuthorizedProxyRequest(ctx, req, res, live)
39
- if (!auth.ok) return
40
- const pool = auth.pool
41
-
42
- let bodyBuf
43
- try {
44
- bodyBuf = await readBody(req, 64 * 1024 * 1024)
45
- } catch (err) {
46
- return writeJson(res, 400, { ok: false, error: String(err?.message || err) })
47
- }
48
-
49
- let body
50
- try {
51
- body = JSON.parse(bodyBuf.toString('utf8') || '{}')
52
- } catch {
53
- return writeJson(res, 400, { ok: false, error: 'Invalid JSON request body' })
54
- }
55
-
56
- const cfg = plainConfig(live())
57
- const pub = publicConfig(cfg)
58
-
59
- const h = req.headers || {}
60
- const sessionId = String(
61
- h['x-dsh-session-id'] || h['x-session-id'] || h['session-id'] || h['session_id'] ||
62
- h['x-session-affinity'] || h['x-client-request-id'] ||
63
- body?.session_id || body?.conversation_id || body?.prompt_cache_key || body?.chat_id || body?.user || ''
64
- ).trim()
65
-
66
- if (sessionId) {
67
- if (!body.session_id) body.session_id = sessionId
68
- if (!body.conversation_id) body.conversation_id = sessionId
69
- }
70
-
71
- body.model = String(body.model || pub.defaultModel || DEFAULT_MODEL_ID).trim()
72
- if (pub.modelReasoningDefaults?.[body.model] && !body.reasoning_effort) {
73
- body.reasoning_effort = pub.modelReasoningDefaults[body.model]
74
- }
75
-
76
- if (Array.isArray(body?.messages) && body.messages.length > 0) {
77
- const budgetResult = optimizePromptCacheAndBudget(body.messages, {
78
- offloadSeenImages: pub.offloadSeenImages !== false,
79
- maxPayloadBytes: (Number(pub.maxPayloadMb) || 40) * 1024 * 1024,
80
- })
81
- body.messages = budgetResult.messages
82
- }
83
-
84
- const isStreaming = Boolean(body.stream)
85
- const configuredAccounts = pool.filter(a => a.present && a.value)
86
-
87
- const clientAbortCtrl = new AbortController()
88
- let clientClosed = false
89
- const onClientClose = () => {
90
- clientClosed = true
91
- if (!res.writableFinished) {
92
- clientAbortCtrl.abort()
93
- }
94
- }
95
- if (typeof res.on === 'function') res.on('close', onClientClose)
96
- if (typeof req.on === 'function') req.on('close', onClientClose)
97
-
98
- const cleanupClientListeners = () => {
99
- if (typeof res.off === 'function') res.off('close', onClientClose)
100
- else if (typeof res.removeListener === 'function') res.removeListener('close', onClientClose)
101
- if (typeof req.off === 'function') req.off('close', onClientClose)
102
- else if (typeof req.removeListener === 'function') req.removeListener('close', onClientClose)
103
- }
104
-
105
- const maxRetries = Math.max(1, configuredAccounts.length)
106
- let attempt = 0
107
- let lastError = null
108
- let hadTimeout = false
109
- let lastTargetAccount = null
110
- let lastRetryAfter = null
111
-
112
- while (attempt < maxRetries) {
113
- attempt += 1
114
-
115
- if (clientAbortCtrl.signal.aborted || clientClosed) {
116
- cleanupClientListeners()
117
- return
118
- }
119
-
120
- const routeResult = resolveSessionAccount(sessionId, pool, pub.activeAccount, pub.accountMode, body?.model, pub.modelRoutes)
121
- const targetAccount = routeResult.account
122
- lastTargetAccount = targetAccount
123
-
124
- if (!targetAccount || !targetAccount.value) {
125
- cleanupClientListeners()
126
- return writeJson(res, 503, {
127
- error: {
128
- message: 'No available ClinePass API keys configured in pool',
129
- type: 'service_unavailable',
130
- code: 'no_keys_available'
131
- }
132
- })
133
- }
134
-
135
- const accountId = targetAccount.apiKeyEnv || targetAccount.id || targetAccount.label
136
- const upstreamUrl = `${pub.baseUrl}/chat/completions`
137
- const requestHeaders = {
138
- 'Content-Type': 'application/json',
139
- 'Authorization': `Bearer ${targetAccount.value}`,
140
- 'Accept': isStreaming ? 'text/event-stream' : 'application/json',
141
- 'User-Agent': USER_AGENT
142
- }
143
-
144
- const connectTimeoutMs = Math.max(50, Number(pub.timeoutMs) || 15000)
145
- const streamIdleTimeoutMs = Math.max(50, Number(pub.streamIdleTimeoutMs) || 30000)
146
-
147
- const connectAbortCtrl = new AbortController()
148
- const bodyAbortCtrl = new AbortController()
149
- let connectTimedOut = false
150
- const connectTimer = setTimeout(() => {
151
- connectTimedOut = true
152
- connectAbortCtrl.abort(new Error(`Upstream connect timeout after ${connectTimeoutMs}ms`))
153
- }, connectTimeoutMs)
154
-
155
- const fetchSignal = AbortSignal.any([clientAbortCtrl.signal, connectAbortCtrl.signal, bodyAbortCtrl.signal])
156
-
157
- let upstreamRes
158
- try {
159
- upstreamRes = await fetch(upstreamUrl, {
160
- method: 'POST',
161
- headers: requestHeaders,
162
- body: JSON.stringify(body),
163
- signal: fetchSignal,
164
- keepalive: true
165
- })
166
- } catch (fetchErr) {
167
- clearTimeout(connectTimer)
168
- if (clientAbortCtrl.signal.aborted || clientClosed) {
169
- cleanupClientListeners()
170
- return
171
- }
172
- lastError = fetchErr
173
- const isTimeout = connectTimedOut || connectAbortCtrl.signal.aborted || fetchErr.name === 'TimeoutError' || String(fetchErr).includes('timeout')
174
- if (isTimeout) {
175
- hadTimeout = true
176
- markAccountCooldown(accountId, 60000, null, 'upstream_timeout')
177
- if (targetAccount.id && targetAccount.id !== accountId) markAccountCooldown(targetAccount.id, 60000, null, 'upstream_timeout')
178
- ctx?.logger?.warn?.(`[dsh-clinebot:proxy] Account "${accountId}" upstream connect timed out after ${connectTimeoutMs}ms, attempting failover (attempt ${attempt}/${maxRetries})`)
179
- } else {
180
- markAccountCooldown(accountId, 60000, null, 'upstream_fetch_error')
181
- if (targetAccount.id && targetAccount.id !== accountId) markAccountCooldown(targetAccount.id, 60000, null, 'upstream_fetch_error')
182
- ctx?.logger?.warn?.(`[dsh-clinebot:proxy] Account "${accountId}" connect error (${fetchErr.message}), attempting failover (attempt ${attempt}/${maxRetries})`)
183
- }
184
- continue
185
- }
186
- clearTimeout(connectTimer)
187
-
188
- if (clientAbortCtrl.signal.aborted || clientClosed) {
189
- cleanupClientListeners()
190
- return
191
- }
192
-
193
- // Pre-first-token failover: check for retryable non-200 responses before sending ANY headers
194
- if (upstreamRes.status !== 200) {
195
- const isRateLimit = upstreamRes.status === 429 || upstreamRes.status === 402
196
- const isServerErr = upstreamRes.status >= 500 && upstreamRes.status <= 599
197
- const isAuthErr = upstreamRes.status === 401 || upstreamRes.status === 403
198
-
199
- if (isRateLimit || isServerErr || isAuthErr) {
200
- const reason = isRateLimit ? 'upstream_429' : (isAuthErr ? 'upstream_auth' : `upstream_status_${upstreamRes.status}`)
201
- markAccountCooldown(accountId, 60000, null, reason)
202
- if (targetAccount.id && targetAccount.id !== accountId) markAccountCooldown(targetAccount.id, 60000, null, reason)
203
- lastError = new Error(`Upstream rejected with HTTP ${upstreamRes.status}`)
204
- lastError.status = upstreamRes.status
205
- const retryAfter = upstreamRes.headers?.get?.('retry-after')
206
- if (retryAfter) lastRetryAfter = retryAfter
207
- if (upstreamRes.body) {
208
- try {
209
- if (typeof upstreamRes.body.cancel === 'function') upstreamRes.body.cancel().catch(() => {})
210
- else if (typeof upstreamRes.body.destroy === 'function') upstreamRes.body.destroy()
211
- } catch {}
212
- }
213
- ctx?.logger?.warn?.(`[dsh-clinebot:proxy] Account "${accountId}" returned HTTP ${upstreamRes.status}, attempting failover (attempt ${attempt}/${maxRetries})`)
214
- continue
215
- }
216
-
217
- // Non-retryable client error (e.g. 400 Bad Request)
218
- cleanupClientListeners()
219
- const errText = await upstreamRes.text().catch(() => '')
220
- res.statusCode = upstreamRes.status
221
- res.setHeader('Content-Type', 'application/json; charset=utf-8')
222
- res.end(errText || JSON.stringify({ error: { message: `Upstream error ${upstreamRes.status}` } }))
223
- return
224
- }
225
-
226
- // Streaming response with pre-first-token buffering (#212)
227
- if (isStreaming && upstreamRes.status === 200 && upstreamRes.body) {
228
- const reader = upstreamRes.body.getReader()
229
- const decoder = new TextDecoder('utf8')
230
-
231
- let firstReadResult = null
232
- let preTokenError = null
233
-
234
- const ttftAbortCtrl = new AbortController()
235
- const ttftTimer = setTimeout(() => {
236
- ttftAbortCtrl.abort(new Error(`TTFT timeout waiting for first stream chunk after ${connectTimeoutMs}ms`))
237
- }, connectTimeoutMs)
238
-
239
- try {
240
- const firstReadPromise = reader.read()
241
- const timeoutPromise = new Promise((_, reject) => {
242
- ttftAbortCtrl.signal.addEventListener('abort', () => reject(ttftAbortCtrl.signal.reason), { once: true })
243
- })
244
- firstReadResult = await Promise.race([firstReadPromise, timeoutPromise])
245
- } catch (err) {
246
- preTokenError = err
247
- } finally {
248
- clearTimeout(ttftTimer)
249
- }
250
-
251
- if (clientAbortCtrl.signal.aborted || clientClosed) {
252
- cleanupClientListeners()
253
- try { reader.cancel('client aborted').catch(() => {}) } catch {}
254
- return
255
- }
256
-
257
- let earlyError = null
258
- if (!preTokenError && firstReadResult?.value) {
259
- const textSample = decoder.decode(firstReadResult.value, { stream: true })
260
- const trimmed = textSample.trim()
261
- if (trimmed.startsWith('{') && trimmed.includes('"error"')) {
262
- try {
263
- const parsed = JSON.parse(trimmed)
264
- if (parsed.error && !parsed.choices) earlyError = new Error(parsed.error.message || 'Upstream error')
265
- } catch {}
266
- } else if (trimmed.startsWith('data:') && trimmed.includes('"error"')) {
267
- try {
268
- const parsed = JSON.parse(trimmed.slice(5).trim())
269
- if (parsed.error && !parsed.choices) earlyError = new Error(parsed.error.message || 'Upstream stream error')
270
- } catch {}
271
- }
272
- } else if (!preTokenError && firstReadResult?.done) {
273
- earlyError = new Error('Upstream stream completed with zero chunks')
274
- }
275
-
276
- if (preTokenError || earlyError) {
277
- if (ttftAbortCtrl.signal.aborted || String(preTokenError?.message).includes('timeout')) hadTimeout = true
278
- try { reader.cancel('pre-first-token error').catch(() => {}) } catch {}
279
- markAccountCooldown(accountId, 60000, null, 'upstream_pre_token_error')
280
- if (targetAccount.id && targetAccount.id !== accountId) markAccountCooldown(targetAccount.id, 60000, null, 'upstream_pre_token_error')
281
- lastError = preTokenError || earlyError
282
- ctx?.logger?.warn?.(`[dsh-clinebot:proxy] Account "${accountId}" failed before first token (${lastError.message}), attempting failover (attempt ${attempt}/${maxRetries})`)
283
- continue
284
- }
285
-
286
- // First token received: commit headers to client now
287
- res.statusCode = 200
288
- res.setHeader('Content-Type', 'text/event-stream; charset=utf-8')
289
- res.setHeader('Cache-Control', 'no-cache, no-transform')
290
- res.setHeader('Connection', 'keep-alive')
291
- if (typeof res.flushHeaders === 'function') {
292
- res.flushHeaders()
293
- }
294
-
295
- let promptTokens = 0
296
- let completionTokens = 0
297
- let reasoningTokens = 0
298
- let totalTokens = 0
299
- let buffer = ''
300
- let streamError = null
301
- let streamCompletedCleanly = false
302
-
303
- let streamAborted = false
304
- const onStreamAbort = () => {
305
- streamAborted = true
306
- try {
307
- reader.cancel('client closed response').catch(() => {})
308
- } catch {}
309
- }
310
-
311
- if (clientAbortCtrl.signal.aborted || clientClosed) {
312
- onStreamAbort()
313
- } else {
314
- clientAbortCtrl.signal.addEventListener('abort', onStreamAbort, { once: true })
315
- }
316
-
317
- let idleTimer = null
318
- let idleTimedOut = false
319
- const resetIdleTimer = () => {
320
- if (idleTimer) clearTimeout(idleTimer)
321
- idleTimer = setTimeout(() => {
322
- idleTimedOut = true
323
- streamError = new Error(`Stream idle timeout after ${streamIdleTimeoutMs}ms`)
324
- try {
325
- reader.cancel('stream idle timeout').catch(() => {})
326
- } catch {}
327
- }, streamIdleTimeoutMs)
328
- }
329
-
330
- const clearIdleTimer = () => {
331
- if (idleTimer) {
332
- clearTimeout(idleTimer)
333
- idleTimer = null
334
- }
335
- }
336
-
337
- const processChunk = (val) => {
338
- if (!res.destroyed && !res.writableEnded) {
339
- res.write(val)
340
- }
341
- const chunkStr = decoder.decode(val, { stream: true })
342
- buffer += chunkStr
343
- const lines = buffer.split('\n')
344
- buffer = lines.pop() || ''
345
- for (const line of lines) {
346
- const trimmed = line.trim()
347
- if (trimmed.startsWith('data:') && trimmed !== 'data: [DONE]') {
348
- try {
349
- const parsed = JSON.parse(trimmed.slice(5).trim())
350
- if (parsed.usage) {
351
- const u = extractUsageMetrics(parsed.usage)
352
- promptTokens = u.promptTokens || promptTokens
353
- completionTokens = u.completionTokens || completionTokens
354
- reasoningTokens = u.reasoningTokens || reasoningTokens
355
- totalTokens = u.totalTokens || totalTokens
356
- }
357
- } catch {}
358
- }
359
- }
360
- }
361
-
362
- if (firstReadResult?.value) {
363
- processChunk(firstReadResult.value)
364
- }
365
-
366
- if (firstReadResult?.done) {
367
- streamCompletedCleanly = true
368
- } else {
369
- try {
370
- resetIdleTimer()
371
- while (true) {
372
- if (streamAborted || clientAbortCtrl.signal.aborted || clientClosed) break
373
- const { done, value } = await reader.read()
374
- if (idleTimedOut || streamError) break
375
- if (done || streamAborted) {
376
- if (done && !streamAborted && !clientClosed && !idleTimedOut && !streamError) {
377
- streamCompletedCleanly = true
378
- }
379
- break
380
- }
381
- resetIdleTimer()
382
- processChunk(value)
383
- if (streamAborted || clientAbortCtrl.signal.aborted || clientClosed) break
384
- }
385
- } catch (err) {
386
- streamError = err
387
- } finally {
388
- clearIdleTimer()
389
- }
390
- }
391
-
392
- clearIdleTimer()
393
- clientAbortCtrl.signal.removeEventListener('abort', onStreamAbort)
394
- cleanupClientListeners()
395
- try {
396
- reader.releaseLock()
397
- } catch {}
398
-
399
- const isStreamFailed = Boolean(streamError || idleTimedOut || clientClosed || clientAbortCtrl.signal.aborted || !streamCompletedCleanly)
400
-
401
- if (!res.destroyed && !res.writableEnded) {
402
- if (isStreamFailed && (idleTimedOut || streamError)) {
403
- try {
404
- const errPayload = JSON.stringify({
405
- error: {
406
- message: streamError?.message || 'Stream interrupted or timed out',
407
- type: 'stream_error',
408
- code: idleTimedOut ? 'stream_idle_timeout' : 'stream_error'
409
- }
410
- })
411
- res.write(`data: ${errPayload}\n\n`)
412
- } catch {}
413
- }
414
- res.end()
415
- }
416
-
417
- recordUsage({
418
- model: body.model,
419
- accountId: targetAccount.id || targetAccount.label,
420
- promptTokens,
421
- completionTokens,
422
- reasoningTokens,
423
- totalTokens: totalTokens || (promptTokens + completionTokens),
424
- isError: isStreamFailed,
425
- statsPath: pub.statsPath
426
- })
427
- return
428
- }
429
-
430
- // Non-streaming JSON response with deadline and failover (#185, #198, #202, #212)
431
- const bodyTimeoutMs = Math.max(50, Number(pub.timeoutMs) || 15000)
432
- let bodyTimedOut = false
433
- let bodyTimer = null
434
- const bodyTimeoutPromise = new Promise((_, reject) => {
435
- bodyTimer = setTimeout(() => {
436
- bodyTimedOut = true
437
- bodyAbortCtrl.abort(new Error(`Upstream body read timed out after ${bodyTimeoutMs}ms`))
438
- reject(new Error(`Upstream body read timed out after ${bodyTimeoutMs}ms`))
439
- }, bodyTimeoutMs)
440
- })
441
-
442
- const MAX_BODY_BYTES = 10 * 1024 * 1024
443
- const contentLength = Number(upstreamRes.headers.get('content-length'))
444
- if (contentLength > MAX_BODY_BYTES) {
445
- clearTimeout(bodyTimer)
446
- cleanupClientListeners()
447
- writeJson(res, 502, { ok: false, error: 'Upstream response exceeded maximum allowed size' })
448
- return
449
- }
450
-
451
- let resText = ''
452
- let readError = null
453
- try {
454
- resText = await Promise.race([
455
- upstreamRes.text(),
456
- bodyTimeoutPromise,
457
- ])
458
- if (resText.length > MAX_BODY_BYTES) {
459
- throw new Error('Upstream response exceeded maximum allowed size')
460
- }
461
- } catch (err) {
462
- readError = err
463
- } finally {
464
- clearTimeout(bodyTimer)
465
- }
466
-
467
- if (clientAbortCtrl.signal.aborted || clientClosed) {
468
- cleanupClientListeners()
469
- return
470
- }
471
-
472
- if (bodyTimedOut || readError) {
473
- const isTimeout = bodyTimedOut || bodyAbortCtrl.signal.aborted
474
- if (isTimeout) hadTimeout = true
475
- markAccountCooldown(accountId, 60000, null, isTimeout ? 'upstream_timeout' : 'upstream_read_error')
476
- if (targetAccount.id && targetAccount.id !== accountId) markAccountCooldown(targetAccount.id, 60000, null, isTimeout ? 'upstream_timeout' : 'upstream_read_error')
477
- lastError = readError || new Error(`Upstream body read timed out after ${bodyTimeoutMs}ms`)
478
- lastError.code = isTimeout ? 'body_read_timeout' : 'body_read_error'
479
- ctx?.logger?.warn?.(`[dsh-clinebot:proxy] Account "${accountId}" body read failed (${lastError.message}), attempting failover (attempt ${attempt}/${maxRetries})`)
480
- continue
481
- }
482
-
483
- let resJson = null
484
- try {
485
- resJson = JSON.parse(resText)
486
- } catch {
487
- resJson = null
488
- }
489
-
490
- if (resJson?.error && !resJson?.choices) {
491
- markAccountCooldown(accountId, 60000, null, 'upstream_error_payload')
492
- if (targetAccount.id && targetAccount.id !== accountId) markAccountCooldown(targetAccount.id, 60000, null, 'upstream_error_payload')
493
- lastError = new Error(resJson.error.message || 'Upstream error response')
494
- ctx?.logger?.warn?.(`[dsh-clinebot:proxy] Account "${accountId}" returned 200 with error (${lastError.message}), attempting failover (attempt ${attempt}/${maxRetries})`)
495
- continue
496
- }
497
-
498
- cleanupClientListeners()
499
- const nonStreamUsage = extractUsageMetrics(resJson?.usage)
500
- recordUsage({
501
- model: body.model,
502
- accountId: targetAccount.id || targetAccount.label,
503
- promptTokens: nonStreamUsage.promptTokens,
504
- completionTokens: nonStreamUsage.completionTokens,
505
- reasoningTokens: nonStreamUsage.reasoningTokens,
506
- totalTokens: nonStreamUsage.totalTokens,
507
- isError: false,
508
- statsPath: pub.statsPath
509
- })
510
-
511
- res.statusCode = 200
512
- res.setHeader('Content-Type', 'application/json; charset=utf-8')
513
- res.end(resText)
514
- return
515
- }
516
-
517
- cleanupClientListeners()
518
- // Exhausted all retries
519
- const is429Exhausted = !hadTimeout && (lastError?.status === 429 || lastError?.status === 402)
520
- recordUsage({
521
- model: body.model,
522
- accountId: lastTargetAccount?.id || lastTargetAccount?.label || 'default',
523
- isError: true,
524
- is429: is429Exhausted,
525
- statsPath: pub.statsPath
526
- })
527
- return sendProxyTerminalError(res, { hadTimeout, is429Exhausted, lastError, lastRetryAfter })
528
- }
529
- }
530
-
531
- export function createModelsHandler(ctx, { live }) {
532
- return async (req, res) => {
533
- if (req.method !== 'GET') {
534
- return writeJson(res, 405, { ok: false, error: 'GET only' })
535
- }
536
-
537
- const auth = await assertAuthorizedProxyRequest(ctx, req, res, live)
538
- if (!auth.ok) return
539
-
540
- const pub = publicConfig(live())
541
- const dynamic = Array.isArray(pub.dynamicModels) ? pub.dynamicModels : []
542
- const custom = Array.isArray(pub.customModels) ? pub.customModels : []
543
- const overrides = Array.isArray(pub.modelContextOverrides) ? pub.modelContextOverrides : []
544
- const allModels = getAllModels(dynamic, overrides, custom)
545
-
546
- const modelsData = allModels.map(m => ({
547
- id: m.id,
548
- object: 'model',
549
- created: 1700000000,
550
- owned_by: 'clinebot',
551
- permission: [],
552
- root: m.id,
553
- parent: null
554
- }))
555
-
556
- writeJson(res, 200, {
557
- object: 'list',
558
- data: modelsData
559
- })
560
- }
561
- }
562
-
563
- export function registerProxyRoutes(ctx, routeEnv) {
564
- const chatHandler = createChatCompletionsHandler(ctx, routeEnv)
565
- const modelsHandler = createModelsHandler(ctx, routeEnv)
566
-
567
- ctx.effect(() => ctx.webServer.register({
568
- kind: 'exact',
569
- path: '/dsh-clinebot/v1/chat/completions',
570
- handler: chatHandler,
571
- }), 'dsh-clinebot: /v1/chat/completions')
572
-
573
- ctx.effect(() => ctx.webServer.register({
574
- kind: 'exact',
575
- path: '/dsh-clinebot/v1/models',
576
- handler: modelsHandler,
577
- }), 'dsh-clinebot: /v1/models')
578
- }