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/lib/cline.js ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Transport for the Cline gateway and its public catalogs. Transport only: no
3
+ * pinning, retry, or failover policy lives here (see `adapter.js`).
4
+ *
5
+ * - `POST {baseURL}/chat/completions` — the gateway
6
+ * - `GET {baseURL}/models` — the gateway's own catalog
7
+ * - `GET api.cline.bot/.../recommended-models` — official subscription list
8
+ * - `GET models.dev/api.json` — community registry, a backstop
9
+ * - `GET openrouter.ai/api/v1/...` — endpoint detail for direct-pipeline models
10
+ *
11
+ * @module dsh-cline-pass/cline
12
+ */
13
+ /** The public Cline endpoint that lists subscription models without auth. */
14
+ export const RECOMMENDED_MODELS_URL = 'https://api.cline.bot/api/v1/ai/cline/recommended-models'
15
+ /** The community model registry. */
16
+ export const MODELS_DEV_URL = 'https://models.dev/api.json'
17
+ /** OpenRouter's public API, used only for direct-pipeline endpoint detail. */
18
+ export const OPENROUTER_API = 'https://openrouter.ai/api/v1'
19
+
20
+ /** Combine a caller signal with a timeout signal. */
21
+ function withTimeout(signal, timeoutMs) {
22
+ const signals = []
23
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) signals.push(AbortSignal.timeout(timeoutMs))
24
+ if (signal !== undefined && signal !== null) signals.push(signal)
25
+ if (signals.length === 0) return undefined
26
+ return signals.length === 1 ? signals[0] : AbortSignal.any(signals)
27
+ }
28
+
29
+ /** Strip a trailing slash so paths can be appended literally. */
30
+ export function trimBase(baseURL) {
31
+ return String(baseURL ?? '').trim().replace(/\/+$/, '')
32
+ }
33
+
34
+ /**
35
+ * The gateway's own chat-completions endpoint for one base URL.
36
+ * @param baseURL - e.g. `https://api.cline.bot/api/v1`.
37
+ */
38
+ export function chatURL(baseURL) {
39
+ return `${trimBase(baseURL)}/chat/completions`
40
+ }
41
+
42
+ /**
43
+ * Send one JSON request and decode whatever comes back.
44
+ * @returns `{ status, ok, json, text }`; never throws for an HTTP error status.
45
+ */
46
+ export async function sendJSON(url, { method = 'GET', apiKey = '', body, signal, timeoutMs = 60000, headers = {} } = {}) {
47
+ const outgoing = {
48
+ method,
49
+ headers: {
50
+ Accept: 'application/json',
51
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
52
+ ...(apiKey === '' ? {} : { Authorization: `Bearer ${apiKey}` }),
53
+ ...headers,
54
+ },
55
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
56
+ signal: withTimeout(signal, timeoutMs),
57
+ }
58
+ const response = await fetch(url, outgoing)
59
+ const text = await response.text()
60
+ let json = null
61
+ if (text.length > 0) {
62
+ try {
63
+ json = JSON.parse(text)
64
+ } catch {
65
+ json = null
66
+ }
67
+ }
68
+ return { status: response.status, ok: response.ok, json, text }
69
+ }
70
+
71
+ /**
72
+ * Send one chat completion, non-streaming.
73
+ * @returns `{ status, ok, json, text }` with the gateway envelope already unwrapped by the caller.
74
+ */
75
+ export function chatCompletion(options) {
76
+ const { baseURL, apiKey, body, signal, timeoutMs = 180000 } = options
77
+ return sendJSON(chatURL(baseURL), { method: 'POST', apiKey, body, signal, timeoutMs })
78
+ }
79
+
80
+ /**
81
+ * Open a streaming chat completion and hand back the live response.
82
+ * @returns the raw `fetch` Response so the caller owns SSE iteration and abort.
83
+ */
84
+ export async function openChatStream({ baseURL, apiKey, body, signal, headers = {} }) {
85
+ return await fetch(chatURL(baseURL), {
86
+ method: 'POST',
87
+ headers: {
88
+ Accept: 'text/event-stream',
89
+ 'Content-Type': 'application/json',
90
+ ...(apiKey === '' ? {} : { Authorization: `Bearer ${apiKey}` }),
91
+ ...headers,
92
+ },
93
+ body: JSON.stringify(body),
94
+ signal,
95
+ })
96
+ }
97
+
98
+ /**
99
+ * List the gateway's own catalog ids.
100
+ * @returns the model ids; an empty array when the call fails or returns nothing.
101
+ */
102
+ export async function fetchGatewayCatalog({ baseURL, apiKey, signal, timeoutMs = 30000 }) {
103
+ try {
104
+ const result = await sendJSON(`${trimBase(baseURL)}/models`, { apiKey, signal, timeoutMs })
105
+ const ids = (result.json?.data ?? []).map((model) => model?.id).filter((id) => typeof id === 'string')
106
+ return ids
107
+ } catch {
108
+ return []
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Collect `cline-pass/*` ids from the official endpoints and the community registry.
114
+ *
115
+ * Additive by design: every source that answers contributes, and a source that
116
+ * fails is skipped rather than failing the scan.
117
+ *
118
+ * @returns `{ models, sources }` — deduplicated ids plus the sources that answered.
119
+ */
120
+ export async function fetchOfficialModels({ signal, timeoutMs = 30000, fetchImpl = fetch } = {}) {
121
+ const found = new Set()
122
+ const sources = []
123
+ const add = (value) => {
124
+ const id = typeof value === 'string' ? value : value?.id
125
+ if (typeof id !== 'string') return
126
+ const normalized = id.trim().toLowerCase()
127
+ if (normalized.startsWith('cline-pass/')) found.add(normalized)
128
+ }
129
+ try {
130
+ const response = await fetchImpl(RECOMMENDED_MODELS_URL, { signal: withTimeout(signal, timeoutMs) })
131
+ if (response.ok) {
132
+ const json = await response.json()
133
+ const list = json?.clinePass ?? json?.data?.clinePass
134
+ if (Array.isArray(list) && list.length > 0) {
135
+ list.forEach(add)
136
+ sources.push('cline.api')
137
+ }
138
+ }
139
+ } catch { /* source unavailable */ }
140
+ try {
141
+ const response = await fetchImpl(MODELS_DEV_URL, { signal: withTimeout(signal, timeoutMs) })
142
+ if (response.ok) {
143
+ const json = await response.json()
144
+ const provider = json?.providers?.['cline-pass'] ?? json?.['cline-pass']
145
+ if (provider?.models !== undefined && provider.models !== null) {
146
+ Object.keys(provider.models).forEach((id) => add(id.startsWith('cline-pass/') ? id : `cline-pass/${id}`))
147
+ sources.push('models.dev')
148
+ }
149
+ }
150
+ } catch { /* source unavailable */ }
151
+ return { models: [...found].filter((id) => /^cline-pass\/[a-z0-9._-]+$/.test(id)), sources }
152
+ }
153
+
154
+ /**
155
+ * Resolve a canonical slug against OpenRouter's public catalog, tolerating the
156
+ * hyphen differences between the gateway's slug and OpenRouter's id.
157
+ * @returns the real OpenRouter id, or null.
158
+ */
159
+ export async function resolveOpenRouterSlug(slug, { signal, timeoutMs = 30000 } = {}) {
160
+ try {
161
+ const result = await sendJSON(`${OPENROUTER_API}/models`, { signal, timeoutMs })
162
+ const ids = (result.json?.data ?? []).map((model) => model?.id).filter((id) => typeof id === 'string')
163
+ const normalize = (value) => String(value).toLowerCase().replace(/[^a-z0-9]/g, '')
164
+ return ids.find((id) => id === slug) ?? ids.find((id) => normalize(id) === normalize(slug)) ?? null
165
+ } catch {
166
+ return null
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Per-upstream endpoint detail (context length, recent uptime) for one
172
+ * OpenRouter model. Only meaningful for direct-pipeline models.
173
+ * @returns `{ slug, endpoints }`, where endpoints is `[{ slug, name, endpoints, context, uptime }]`.
174
+ */
175
+ export async function openRouterEndpoints(slug, { signal, timeoutMs = 30000 } = {}) {
176
+ const real = await resolveOpenRouterSlug(slug, { signal, timeoutMs })
177
+ if (real === null) return { slug, endpoints: [] }
178
+ try {
179
+ const result = await sendJSON(`${OPENROUTER_API}/models/${real}/endpoints`, { signal, timeoutMs })
180
+ const detail = new Map()
181
+ for (const entry of result.json?.data?.endpoints ?? []) {
182
+ const providerSlug = String(entry?.tag ?? '').split('/')[0] || String(entry?.provider_name ?? '').toLowerCase().replace(/\s+/g, '-')
183
+ if (providerSlug === '') continue
184
+ const current = detail.get(providerSlug) ?? { slug: providerSlug, name: String(entry?.provider_name ?? providerSlug), endpoints: 0, context: 0, uptime: 0 }
185
+ current.endpoints += 1
186
+ current.context = Math.max(current.context, Number(entry?.context_length ?? 0))
187
+ current.uptime = Math.max(current.uptime, Math.round(Number(entry?.uptime_last_30m ?? 0)))
188
+ detail.set(providerSlug, current)
189
+ }
190
+ return { slug: real, endpoints: [...detail.values()] }
191
+ } catch {
192
+ return { slug: real, endpoints: [] }
193
+ }
194
+ }
package/lib/engine.js ADDED
@@ -0,0 +1,405 @@
1
+ /**
2
+ * The engine: account selection, pinned request chains with failover, probing,
3
+ * validation, and request recording. It owns every network call, so the adapter
4
+ * and the management tools apply identical pinning semantics.
5
+ *
6
+ * Failover contract: a candidate is abandoned only while nothing has been
7
+ * delivered yet. A non-2xx, a transport failure, or an error payload before the
8
+ * first content delta moves to the next candidate; once a chunk has been
9
+ * produced the stream is the answer, and a later failure is reported as such.
10
+ *
11
+ * @module dsh-cline-pass/engine
12
+ */
13
+ import { LlmError } from '@deepseek-ai/dsh-llm'
14
+ import {
15
+ chatCompletion,
16
+ fetchGatewayCatalog,
17
+ fetchOfficialModels,
18
+ openChatStream,
19
+ openRouterEndpoints,
20
+ } from './cline.js'
21
+ import {
22
+ buildAttempts,
23
+ classifyUpstreamError,
24
+ errorText,
25
+ extractAvailableProviders,
26
+ injectPrefs,
27
+ mergeUpstreams,
28
+ parseRouting,
29
+ parseTier0,
30
+ unwrapEnvelope,
31
+ } from './protocol.js'
32
+
33
+ /** How many upstreams are validated concurrently. */
34
+ const VALIDATE_CONCURRENCY = 5
35
+
36
+ /**
37
+ * Parse an SSE body into decoded JSON frames.
38
+ * @param body - a web ReadableStream of UTF-8 bytes.
39
+ * @returns every `data:` payload up to `[DONE]`.
40
+ */
41
+ export async function* parseServerSentEvents(body) {
42
+ const decoder = new TextDecoder()
43
+ let buffer = ''
44
+ for await (const bytes of body) {
45
+ buffer += decoder.decode(bytes, { stream: true })
46
+ let index = buffer.indexOf('\n')
47
+ while (index >= 0) {
48
+ const line = buffer.slice(0, index).replace(/\r$/, '')
49
+ buffer = buffer.slice(index + 1)
50
+ index = buffer.indexOf('\n')
51
+ if (line.length === 0 || line.startsWith(':')) continue
52
+ if (!line.startsWith('data:')) continue
53
+ const payload = line.slice(5).trim()
54
+ if (payload === '[DONE]') {
55
+ yield '[DONE]'
56
+ return
57
+ }
58
+ try {
59
+ yield JSON.parse(payload)
60
+ } catch { /* an unparseable frame carries nothing to deliver */ }
61
+ }
62
+ }
63
+ const tail = buffer.trim()
64
+ if (tail.startsWith('data:')) {
65
+ const payload = tail.slice(5).trim()
66
+ if (payload === '[DONE]') {
67
+ yield '[DONE]'
68
+ return
69
+ }
70
+ if (payload.length > 0) {
71
+ try {
72
+ yield JSON.parse(payload)
73
+ } catch { /* see above */ }
74
+ }
75
+ }
76
+ throw new LlmError('stream ended without [DONE]', 'STREAM_CLOSED')
77
+ }
78
+
79
+ /**
80
+ * Create the engine.
81
+ *
82
+ * @param options.resolveAccount - async `() => { name, key, baseURL }`; called
83
+ * once per attempt, so round-robin pools rotate per attempt.
84
+ * @param options.store - the observation store (probe results, history).
85
+ * @param options.logger - optional Cordis logger.
86
+ * @param options.attemptTimeoutMs - per-attempt budget for a chat request.
87
+ * @param options.probeTimeoutMs - budget for probe/validate requests.
88
+ */
89
+ export function createEngine({
90
+ resolveAccount,
91
+ store,
92
+ logger,
93
+ attemptTimeoutMs = 180000,
94
+ probeTimeoutMs = 60000,
95
+ }) {
96
+ /**
97
+ * One pinned request, non-streaming.
98
+ * @returns `{ status, ok, json, text, routing, attempt, account }`.
99
+ */
100
+ async function attemptChat(model, body, attempt, { signal, timeoutMs = attemptTimeoutMs } = {}) {
101
+ const account = await resolveAccount()
102
+ const pinned = injectPrefs(body, store.metaOf(model), attempt)
103
+ const started = Date.now()
104
+ const result = await chatCompletion({
105
+ baseURL: account.baseURL,
106
+ apiKey: account.key,
107
+ body: pinned,
108
+ signal,
109
+ timeoutMs,
110
+ })
111
+ const payload = unwrapEnvelope(result.json)
112
+ const failed = result.ok !== true || (payload?.error !== undefined && payload?.choices === undefined)
113
+ return {
114
+ attempt,
115
+ account,
116
+ status: failed ? (payload?.error === undefined ? result.status : 502) : 200,
117
+ ok: !failed,
118
+ json: payload,
119
+ text: result.text,
120
+ ms: Date.now() - started,
121
+ routing: failed ? {} : parseRouting(payload),
122
+ error: failed ? (errorText(payload?.error) || result.text.slice(0, 400) || `HTTP ${result.status}`) : '',
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Run one model's ordered candidate chain until an attempt succeeds.
128
+ * @returns `{ ok, result?, trace, error }` — `result` is the winning attempt.
129
+ */
130
+ async function runChain(model, body, pinConfig, { signal, timeoutMs } = {}) {
131
+ const trace = []
132
+ let last = null
133
+ for (const attempt of buildAttempts(pinConfig)) {
134
+ const outcome = await attemptChat(model, body, attempt, { signal, timeoutMs })
135
+ trace.push({ upstream: attempt.upstream ?? '(auto)', status: outcome.status, ms: outcome.ms, note: outcome.error === '' ? 'ok' : outcome.error.slice(0, 160) })
136
+ last = outcome
137
+ if (outcome.ok) return { ok: true, result: outcome, trace, error: '' }
138
+ // A channel that refuses a strict pin is learned as unusable for it.
139
+ if (attempt.upstream !== null && outcome.error !== '') store.learnUpstream(model, attempt.upstream, classifyUpstreamError(outcome.error), outcome.error, outcome.ms)
140
+ if (attempt.upstream === null && (attempt.excludeList ?? []).length > 0 && outcome.error !== '') learnFromError(model, outcome.error)
141
+ }
142
+ return { ok: false, result: last, trace, error: last?.error ?? 'no candidate succeeded' }
143
+ }
144
+
145
+ /** Merge a gateway error's own provider list into the model's channel list. */
146
+ function learnFromError(model, message) {
147
+ const match = /Available providers are:\s*([^.]+)/.exec(String(message ?? ''))
148
+ if (match === null) return
149
+ const tokens = match[1].split(/,\s*/).map((token) => token.trim()).filter((token) => /^[a-z0-9][a-z0-9-]*$/.test(token))
150
+ if (tokens.length === 0) return
151
+ const meta = store.metaOf(model)
152
+ const merged = mergeUpstreams(meta.upstreams ?? [], tokens)
153
+ if (merged.length !== (meta.upstreams ?? []).length) store.learn(model, { upstreams: merged })
154
+ }
155
+
156
+ /**
157
+ * Probe one model: detect its pipeline, read back the upstream it used, and
158
+ * harvest the precise channel list for that pipeline. Costs one tiny request.
159
+ */
160
+ async function probe(model, { signal, timeoutMs = attemptTimeoutMs } = {}) {
161
+ const account = await resolveAccount()
162
+ const started = Date.now()
163
+ const result = await chatCompletion({
164
+ baseURL: account.baseURL,
165
+ apiKey: account.key,
166
+ body: { model, messages: [{ role: 'user', content: 'Reply with the word OK' }], max_tokens: 256 },
167
+ signal,
168
+ timeoutMs,
169
+ })
170
+ const payload = unwrapEnvelope(result.json)
171
+ const ms = Date.now() - started
172
+ if (payload?.error !== undefined && payload?.choices === undefined) {
173
+ const message = errorText(payload.error)
174
+ store.learn(model, { ok: false, lastError: message, lastMs: ms })
175
+ return { ok: false, ms, model, error: message }
176
+ }
177
+ const routing = parseRouting(payload)
178
+ const pipeline = routing.pipeline
179
+ let harvested = null
180
+ if (pipeline !== null) harvested = await harvest(model, pipeline, account, { signal, timeoutMs: probeTimeoutMs })
181
+ let endpoints = []
182
+ let openrouterSlug = null
183
+ if (pipeline !== 'planner' && typeof routing.canonicalSlug === 'string') {
184
+ const detail = await openRouterEndpoints(routing.canonicalSlug, { signal, timeoutMs: probeTimeoutMs })
185
+ endpoints = detail.endpoints
186
+ openrouterSlug = detail.slug
187
+ }
188
+ const previous = store.metaOf(model)
189
+ const upstreamDetail = { ...(previous.upstreamDetail ?? {}) }
190
+ for (const entry of endpoints) upstreamDetail[entry.slug] = entry
191
+ const upstreams = pipeline === 'planner'
192
+ ? mergeUpstreams(harvested, routing.fallbacks)
193
+ : mergeUpstreams(routing.fallbacks, harvested, Object.keys(upstreamDetail))
194
+ const tier0 = [...new Set([...(previous.tier0 ?? []), ...parseTier0(routing.plan)])]
195
+ const meta = store.learn(model, {
196
+ ok: true,
197
+ pipeline,
198
+ pinnable: pipeline !== null,
199
+ availableProviders: harvested ?? previous.availableProviders ?? [],
200
+ canonicalSlug: routing.canonicalSlug ?? previous.canonicalSlug ?? null,
201
+ openrouterSlug: openrouterSlug ?? previous.openrouterSlug ?? null,
202
+ upstreamDetail,
203
+ upstreams,
204
+ tier0,
205
+ lastProvider: routing.finalProvider ?? previous.lastProvider ?? null,
206
+ lastMs: ms,
207
+ probedAt: Date.now(),
208
+ lastError: '',
209
+ })
210
+ return {
211
+ ok: true,
212
+ ms,
213
+ model,
214
+ error: '',
215
+ pipeline: meta.pipeline,
216
+ pinnable: meta.pinnable === true,
217
+ canonicalSlug: meta.canonicalSlug ?? '',
218
+ lastProvider: meta.lastProvider ?? '',
219
+ upstreams: meta.upstreams ?? [],
220
+ availableProviders: meta.availableProviders ?? [],
221
+ tier0: meta.tier0 ?? [],
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Harvest the exact channel list for one pipeline by pinning a channel that
227
+ * cannot exist: the router fails before spending a token and names every
228
+ * provider it could have used.
229
+ */
230
+ async function harvest(model, pipeline, account, { signal, timeoutMs = probeTimeoutMs } = {}) {
231
+ const base = { model, messages: [{ role: 'user', content: 'hi' }], max_tokens: 16 }
232
+ const body = pipeline === 'planner'
233
+ ? { ...base, providerOptions: { gateway: { only: ['__probe__'] } } }
234
+ : { ...base, provider: { only: ['__probe__'] } }
235
+ try {
236
+ const result = await chatCompletion({ baseURL: account.baseURL, apiKey: account.key, body, signal, timeoutMs })
237
+ const payload = unwrapEnvelope(result.json)
238
+ const message = errorText(payload?.error) || result.text
239
+ return extractAvailableProviders(message, pipeline)
240
+ } catch {
241
+ return null
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Test every known channel of a model once and record the verdicts.
247
+ * @returns `{ results: [{ upstream, status, ms, note }], summary }`.
248
+ */
249
+ async function validate(model, { signal, timeoutMs = probeTimeoutMs } = {}) {
250
+ const meta = store.metaOf(model)
251
+ const list = Array.isArray(meta.upstreams) ? meta.upstreams : []
252
+ const pipeline = meta.pipeline ?? null
253
+ const results = []
254
+ for (let index = 0; index < list.length; index += VALIDATE_CONCURRENCY) {
255
+ const batch = list.slice(index, index + VALIDATE_CONCURRENCY)
256
+ const settled = await Promise.all(batch.map(async (upstream) => {
257
+ const started = Date.now()
258
+ const base = { model, messages: [{ role: 'user', content: 'hi' }], max_tokens: 16 }
259
+ const body = pipeline === 'planner'
260
+ ? { ...base, providerOptions: { gateway: { only: [upstream] } } }
261
+ : { ...base, provider: { only: [upstream] } }
262
+ try {
263
+ const account = await resolveAccount()
264
+ const result = await chatCompletion({ baseURL: account.baseURL, apiKey: account.key, body, signal, timeoutMs })
265
+ const payload = unwrapEnvelope(result.json)
266
+ const ms = Date.now() - started
267
+ let status = 'unknown'
268
+ let note = ''
269
+ if (payload?.error !== undefined && payload?.choices === undefined) {
270
+ note = errorText(payload.error)
271
+ status = classifyUpstreamError(note)
272
+ } else if (payload?.choices !== undefined) {
273
+ status = 'ok'
274
+ }
275
+ return { upstream, status, ms, note: note.slice(0, 160) }
276
+ } catch (error) {
277
+ return { upstream, status: 'unknown', ms: Date.now() - started, note: errorText(error).slice(0, 160) }
278
+ }
279
+ }))
280
+ for (const result of settled) {
281
+ store.learnUpstream(model, result.upstream, result.status, result.note, result.ms)
282
+ results.push(result)
283
+ }
284
+ }
285
+ const summary = { ok: 0, limited: 0, bad: 0, auth: 0, unknown: 0 }
286
+ for (const result of results) summary[result.status] = (summary[result.status] ?? 0) + 1
287
+ store.learn(model, { validatedAt: Date.now() })
288
+ return { results, summary }
289
+ }
290
+
291
+ /**
292
+ * Send one small chat through a (possibly temporary) pin configuration and
293
+ * report what served it. Nothing is persisted.
294
+ */
295
+ async function test(model, { upstreams, exclude, signal } = {}) {
296
+ const pin = currentPin(model, { upstreams, exclude })
297
+ const started = Date.now()
298
+ const outcome = await runChain(model, { model, messages: [{ role: 'user', content: 'Reply with the word OK' }], max_tokens: 256 }, pin, { signal, timeoutMs: attemptTimeoutMs })
299
+ const ms = Date.now() - started
300
+ if (!outcome.ok) {
301
+ record(model, { provider: null, attempts: outcome.trace.map((row) => row.upstream), ms, stream: false, error: outcome.error, account: outcome.result?.account?.name ?? null })
302
+ return { ok: false, ms, model, error: outcome.error, targets: pin.upstreams ?? [], excluded: pin.exclude ?? [], trace: outcome.trace, actual: '', content: '', account: outcome.result?.account?.name ?? '' }
303
+ }
304
+ const routing = outcome.result.routing
305
+ record(model, { provider: routing.finalProvider, canonical: routing.canonicalSlug, attempts: outcome.trace.map((row) => row.upstream), ms, stream: false, error: null, account: outcome.result.account?.name ?? null })
306
+ return {
307
+ ok: true,
308
+ ms,
309
+ model,
310
+ error: '',
311
+ targets: pin.upstreams ?? [],
312
+ excluded: pin.exclude ?? [],
313
+ trace: outcome.trace,
314
+ actual: routing.finalProvider ?? '',
315
+ actualName: routing.finalProviderName ?? '',
316
+ pipeline: routing.pipeline ?? '',
317
+ pinnable: routing.pipeline !== null,
318
+ canonicalSlug: routing.canonicalSlug ?? '',
319
+ account: outcome.result.account?.name ?? '',
320
+ content: String(routing.content ?? '').slice(0, 120),
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Check one account key against the gateway with a single small request.
326
+ *
327
+ * A key that authenticates but whose model still errors (a reasoning model
328
+ * burning `max_tokens` into an empty completion, for example) counts as
329
+ * authorized: only a real authentication failure is reported as one.
330
+ */
331
+ async function testAccount({ key, baseURL, model, signal, timeoutMs = probeTimeoutMs }) {
332
+ const started = Date.now()
333
+ try {
334
+ const result = await chatCompletion({
335
+ baseURL,
336
+ apiKey: key,
337
+ body: { model, messages: [{ role: 'user', content: 'Say OK' }], max_tokens: 512 },
338
+ signal,
339
+ timeoutMs,
340
+ })
341
+ const payload = unwrapEnvelope(result.json)
342
+ const ms = Date.now() - started
343
+ if (payload?.error !== undefined && payload?.choices === undefined) {
344
+ const message = errorText(payload.error)
345
+ const authFailed = /unauthorized|re-authenticate|invalid\s*api|401/i.test(message)
346
+ return { ok: !authFailed, authorized: !authFailed, ms, model, error: message }
347
+ }
348
+ return { ok: true, authorized: true, ms, model, error: '' }
349
+ } catch (error) {
350
+ return { ok: false, authorized: false, ms: Date.now() - started, model, error: errorText(error) }
351
+ }
352
+ }
353
+
354
+ /** Resolve a per-call pin override against the stored configuration. */
355
+ function currentPin(model, override = {}) {
356
+ const stored = pinOf(model)
357
+ return {
358
+ upstreams: Array.isArray(override.upstreams) ? override.upstreams.map(String) : stored.upstreams,
359
+ exclude: Array.isArray(override.exclude) ? override.exclude.map(String) : stored.exclude,
360
+ pinMode: stored.pinMode,
361
+ sort: stored.sort,
362
+ }
363
+ }
364
+
365
+ /** The configured pin for one model, read through the plugin's live config. */
366
+ let pinReader = () => ({ upstreams: [], exclude: [], pinMode: 'strict', sort: null })
367
+ const pinOf = (model) => pinReader(model)
368
+
369
+ /** Record one request in the observation store. */
370
+ function record(model, info) {
371
+ store.record({ model, ...info })
372
+ }
373
+
374
+ return {
375
+ /** Replace the function that reads a model's configured pin. */
376
+ setPinReader(reader) {
377
+ pinReader = reader
378
+ },
379
+ /** Refresh the gateway's own catalog ids. */
380
+ async gatewayCatalog(options = {}) {
381
+ const account = await resolveAccount()
382
+ return await fetchGatewayCatalog({ baseURL: account.baseURL, apiKey: account.key, ...options })
383
+ },
384
+ /** Refresh the official subscription model list. */
385
+ async officialModels(options = {}) {
386
+ return await fetchOfficialModels(options)
387
+ },
388
+ /** The resolved account for the next request (round-robin aware). */
389
+ resolveAccount,
390
+ attemptChat,
391
+ runChain,
392
+ probe,
393
+ validate,
394
+ test,
395
+ testAccount,
396
+ currentPin,
397
+ record,
398
+ learnFromError,
399
+ chatCompletion,
400
+ openChatStream,
401
+ parseServerSentEvents,
402
+ buildAttempts,
403
+ injectPrefs,
404
+ }
405
+ }