@ebony-vinyl/dsh-our-free-model 2.7.4 → 2.8.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.
- package/README.md +23 -8
- package/client.js +894 -6
- package/cordis.patch.yml +1 -1
- package/index.js +248 -11
- package/package.json +6 -2
- package/src/adapter.js +54 -6
- package/src/aggregate.js +53 -13
- package/src/bun-fetch.js +33 -10
- package/src/catalog.js +238 -58
- package/src/core.js +838 -0
- package/src/egress.js +204 -3
- package/src/engine-parts/win32-x64.000.part +0 -0
- package/src/engine-parts/win32-x64.001.part +0 -0
- package/src/engine-parts/win32-x64.002.part +0 -0
- package/src/engine-parts/win32-x64.003.part +0 -0
- package/src/engine-parts/win32-x64.004.part +0 -0
- package/src/engine-parts/win32-x64.005.part +0 -0
- package/src/engine-parts/win32-x64.006.part +0 -0
- package/src/extensions.js +1888 -0
- package/src/feed.js +6 -0
- package/src/http.js +15 -2
- package/src/kilo.js +55 -5
- package/src/magpie-engine.js +61 -2
- package/src/magpie-models.js +31 -3
- package/src/model-test.js +3 -1
- package/src/o2a.js +956 -0
- package/src/open-external.js +43 -5
- package/src/plugins.js +1240 -0
- package/src/providers/registry.json +72 -30
- package/src/semver.js +40 -0
- package/src/store.js +17 -1
- package/src/stream.js +39 -4
- package/src/subscription-completion.js +12 -3
- package/src/updater.js +5 -33
- package/src/upstream.js +283 -363
- package/vendor/channel-pack/NOTICE.md +1 -1
- package/vendor/channel-pack/pack.js +7242 -3952
- package/vendor/magpie/NOTICE.md +159 -6
- package/vendor/magpie/engine.json +58 -27
- package/src/core/completion.js +0 -196
- package/src/core/runtime.js +0 -456
- package/src/core/stats.js +0 -98
- package/src/engine-parts/win32-x64.007.part +0 -0
- package/src/engine-parts/win32-x64.008.part +0 -0
package/src/feed.js
CHANGED
|
@@ -109,6 +109,11 @@ function parseItem(row, now) {
|
|
|
109
109
|
&& /^https?:\/\//i.test(row.link.url)) {
|
|
110
110
|
link = { url: row.link.url, label: typeof row.link.label === 'string' ? row.link.label.slice(0, 100) : '' }
|
|
111
111
|
}
|
|
112
|
+
// `holdSeconds` asks the client to keep the modal's close control locked for
|
|
113
|
+
// a few seconds so the body is actually read (0/absent closes immediately).
|
|
114
|
+
// The ceiling keeps an owner from writing "hold for a week".
|
|
115
|
+
const holdSeconds = row.holdSeconds
|
|
116
|
+
const hold = Number.isInteger(holdSeconds) && holdSeconds >= 0 && holdSeconds <= 30 ? holdSeconds : 0
|
|
112
117
|
return {
|
|
113
118
|
id,
|
|
114
119
|
title,
|
|
@@ -118,6 +123,7 @@ function parseItem(row, now) {
|
|
|
118
123
|
...expiresAt === undefined ? {} : { expiresAt },
|
|
119
124
|
...row.pinned === true ? { pinned: true } : {},
|
|
120
125
|
...link === undefined ? {} : { link },
|
|
126
|
+
...(hold > 0 ? { holdSeconds: hold } : {}),
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
129
|
|
package/src/http.js
CHANGED
|
@@ -419,7 +419,12 @@ function emptyBodyFailure(status, retryAfterMs, imageContent) {
|
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
export async function postStreamed({ path, body, session, attributionUserAgent, signal, onData, timeoutMs = 300000 }) {
|
|
422
|
-
|
|
422
|
+
// The Messages wire carries the 2.x client's beta header (o2a/headers.js);
|
|
423
|
+
// the path is the only wire signal this layer sees. The project id derives
|
|
424
|
+
// from the same canonical session, so one conversation's retries,
|
|
425
|
+
// continuations and refreshes share one upstream quota bucket (PR #757).
|
|
426
|
+
const canonical = truncateSession(session)
|
|
427
|
+
const headers = gatewayHeaders({ session: canonical, project: canonical, stream: true, anthropic: path.includes('/messages') })
|
|
423
428
|
headers['user-agent'] = userAgentWith(attributionUserAgent)
|
|
424
429
|
let response
|
|
425
430
|
try {
|
|
@@ -429,6 +434,13 @@ export async function postStreamed({ path, body, session, attributionUserAgent,
|
|
|
429
434
|
// `TimeoutError`/user Error rather than `AbortError` — testing the name alone
|
|
430
435
|
// reported a cancelled turn as `TRANSPORT`, which is retryable.
|
|
431
436
|
if (signal?.aborted === true || error?.name === 'AbortError') throw new UpstreamError('request aborted', CODE.aborted)
|
|
437
|
+
// A lane-configuration refusal (a missing Bun, an unusable proxy) is not an
|
|
438
|
+
// "upstream request failed": the sentence the lane wrote is already the
|
|
439
|
+
// actionable answer, so it ships unwrapped, with its flag intact for the
|
|
440
|
+
// surfaces that distinguish "fix your environment" from "upstream broke".
|
|
441
|
+
if (error?.laneConfig === true) {
|
|
442
|
+
throw new UpstreamError(String(error?.message ?? error), CODE.transport, { laneConfig: true })
|
|
443
|
+
}
|
|
432
444
|
throw new UpstreamError(`our-free-model: upstream request failed: ${error?.message ?? error}${transportCause(error)}`, CODE.transport)
|
|
433
445
|
}
|
|
434
446
|
|
|
@@ -623,7 +635,8 @@ export async function readSse(source, onData, signal, timeoutMs = 300000) {
|
|
|
623
635
|
|
|
624
636
|
/** Fetch a small JSON document from the gateway with the fingerprint headers. */
|
|
625
637
|
export async function getJson(path, { session, attributionUserAgent, signal, timeoutMs = 15000 } = {}) {
|
|
626
|
-
const
|
|
638
|
+
const canonical = truncateSession(session ?? '')
|
|
639
|
+
const headers = gatewayHeaders({ session: canonical, project: canonical, stream: false, accept: 'application/json' })
|
|
627
640
|
headers['user-agent'] = userAgentWith(attributionUserAgent)
|
|
628
641
|
const controller = new AbortController()
|
|
629
642
|
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
package/src/kilo.js
CHANGED
|
@@ -25,8 +25,20 @@ import { egressFetch } from './egress.js'
|
|
|
25
25
|
const LISTING_TIMEOUT_MS = 15000
|
|
26
26
|
const TURN_TIMEOUT_MS = 300000
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The gateway base, newest first (`ai-gateway.kilo.ai` is what the official
|
|
30
|
+
* docs publish; the older `api.kilo.ai/api/gateway` path answers the same
|
|
31
|
+
* catalog and stays as the override/fallback). Overridable for the offline
|
|
32
|
+
* suite and the live probes, like the free lane's base.
|
|
33
|
+
*/
|
|
34
|
+
export const KILO_BASE = (process.env.OUR_FREE_MODEL_KILO_BASE ?? 'https://ai-gateway.kilo.ai/api/v1').replace(/\/+$/, '')
|
|
35
|
+
|
|
36
|
+
/** The attribution headers Kilo's own clients send; documented optional, and a
|
|
37
|
+
* gateway that ranks by referrer sees the plugin, not a spoofed editor. */
|
|
38
|
+
const KILO_ATTRIBUTION = {
|
|
39
|
+
'http-referer': 'https://kilo.ai/',
|
|
40
|
+
'x-title': 'Our Free Model',
|
|
41
|
+
}
|
|
30
42
|
|
|
31
43
|
/**
|
|
32
44
|
* The lane's transport is `egressFetch`, exactly like the free lane's: an
|
|
@@ -37,7 +49,13 @@ function kiloFetch(url, init) {
|
|
|
37
49
|
return egressFetch(url, init)
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
/**
|
|
52
|
+
/** The cached listing document and its validator, for conditional rounds. */
|
|
53
|
+
let listingCache = null
|
|
54
|
+
let listingETag = null
|
|
55
|
+
|
|
56
|
+
/** One listing round: `GET {base}/models`, conditionally when the gateway
|
|
57
|
+
* named a validator last time. Returns the parsed JSON document; a `304`
|
|
58
|
+
* answers with the cached one. */
|
|
41
59
|
export async function fetchKiloListing({ signal, timeoutMs = LISTING_TIMEOUT_MS } = {}) {
|
|
42
60
|
const controller = new AbortController()
|
|
43
61
|
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
@@ -46,15 +64,27 @@ export async function fetchKiloListing({ signal, timeoutMs = LISTING_TIMEOUT_MS
|
|
|
46
64
|
const onCallerAbort = () => { callerAborted = true; controller.abort() }
|
|
47
65
|
signal?.addEventListener('abort', onCallerAbort, { once: true })
|
|
48
66
|
try {
|
|
67
|
+
const headers = {
|
|
68
|
+
accept: 'application/json',
|
|
69
|
+
'user-agent': 'dsh-our-free-model',
|
|
70
|
+
...KILO_ATTRIBUTION,
|
|
71
|
+
...(listingETag !== null ? { 'if-none-match': listingETag } : {}),
|
|
72
|
+
}
|
|
49
73
|
const response = await kiloFetch(`${KILO_BASE}/models`, {
|
|
50
|
-
headers
|
|
74
|
+
headers,
|
|
51
75
|
redirect: 'error',
|
|
52
76
|
signal: controller.signal,
|
|
53
77
|
})
|
|
78
|
+
if (response.status === 304 && listingCache !== null) return listingCache
|
|
54
79
|
const text = await response.text()
|
|
55
80
|
let payload
|
|
56
81
|
try { payload = JSON.parse(text) } catch { payload = { error: { message: text.slice(0, 300) || `HTTP ${response.status}` } } }
|
|
57
82
|
if (!response.ok) throw classifyFailure(response.status, payload)
|
|
83
|
+
const etag = response.headers.get('etag')
|
|
84
|
+
if (typeof etag === 'string' && etag !== '') {
|
|
85
|
+
listingETag = etag
|
|
86
|
+
listingCache = payload
|
|
87
|
+
}
|
|
58
88
|
return payload
|
|
59
89
|
} catch (error) {
|
|
60
90
|
if (error instanceof UpstreamError) throw error
|
|
@@ -83,7 +113,7 @@ export async function postKiloStreamed({ body, signal, onData, timeoutMs = TURN_
|
|
|
83
113
|
try {
|
|
84
114
|
response = await kiloFetch(`${KILO_BASE}/chat/completions`, {
|
|
85
115
|
method: 'POST',
|
|
86
|
-
headers: { 'content-type': 'application/json', accept: 'text/event-stream', 'user-agent': 'dsh-our-free-model' },
|
|
116
|
+
headers: { 'content-type': 'application/json', accept: 'text/event-stream', 'user-agent': 'dsh-our-free-model', ...KILO_ATTRIBUTION },
|
|
87
117
|
body: bodyText,
|
|
88
118
|
redirect: 'error',
|
|
89
119
|
signal,
|
|
@@ -151,9 +181,29 @@ function classifyKilo(status, text, retryAfterMs) {
|
|
|
151
181
|
let payload
|
|
152
182
|
try { payload = JSON.parse(text) } catch { payload = { error: { message: text.slice(0, 300) || `HTTP ${status}` } } }
|
|
153
183
|
const type = payload?.error_type ?? payload?.error?.code ?? payload?.error?.type
|
|
184
|
+
const rawMeta = payload?.error?.metadata?.raw ?? payload?.metadata?.raw
|
|
185
|
+
let message = payload?.error?.message ?? (typeof payload?.message === 'string' ? payload.message : undefined)
|
|
186
|
+
if (typeof rawMeta === 'string' && rawMeta.trim() !== '') {
|
|
187
|
+
let rawDetail = ''
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(rawMeta)
|
|
190
|
+
rawDetail = typeof parsed?.error === 'string' ? parsed.error
|
|
191
|
+
: typeof parsed?.message === 'string' ? parsed.message
|
|
192
|
+
: typeof parsed?.error?.message === 'string' ? parsed.error.message
|
|
193
|
+
: ''
|
|
194
|
+
} catch {
|
|
195
|
+
rawDetail = rawMeta.slice(0, 200).trim()
|
|
196
|
+
}
|
|
197
|
+
const providerName = payload?.error?.metadata?.provider_name ?? payload?.metadata?.provider_name
|
|
198
|
+
const providerSuffix = typeof providerName === 'string' && providerName !== '' ? ` (${providerName})` : ''
|
|
199
|
+
if (rawDetail !== '') {
|
|
200
|
+
message = message ? `${message}: ${rawDetail}${providerSuffix}` : `${rawDetail}${providerSuffix}`
|
|
201
|
+
}
|
|
202
|
+
}
|
|
154
203
|
const shaped = {
|
|
155
204
|
error: {
|
|
156
205
|
...payload?.error,
|
|
206
|
+
...(message !== undefined ? { message } : {}),
|
|
157
207
|
...(type === undefined ? {} : { type: String(type) }),
|
|
158
208
|
},
|
|
159
209
|
}
|
package/src/magpie-engine.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
import { randomBytes, createHash } from 'node:crypto'
|
|
3
|
-
import { existsSync, readFileSync, createReadStream } from 'node:fs'
|
|
3
|
+
import { existsSync, readFileSync, createReadStream, statSync } from 'node:fs'
|
|
4
4
|
import { mkdir, readFile, writeFile, rename, link, unlink, realpath } from 'node:fs/promises'
|
|
5
5
|
import http from 'node:http'
|
|
6
6
|
import net from 'node:net'
|
|
@@ -400,6 +400,11 @@ const engineAppearanceCSS = `
|
|
|
400
400
|
:root[data-ofm-engine] #addSheet > .row-head { height:auto; min-height:32px; gap:8px; }
|
|
401
401
|
:root[data-ofm-engine] #addSheet .find { height:32px; }
|
|
402
402
|
:root[data-ofm-engine] #addSheet .kind { padding-top:16px; }
|
|
403
|
+
/* Upstream's "Compact at" row (#876) shrinks and wraps inside the agent prefs,
|
|
404
|
+
but the settings lists kept it flex:none: at a narrow panel width the fixed
|
|
405
|
+
76px number input sticks past the clipped list and loses half its width.
|
|
406
|
+
The same shrink-and-wrap answer, for the rows upstream missed. */
|
|
407
|
+
:root[data-ofm-engine] .row.pref .compact-at { flex:0 1 auto; min-width:0; flex-wrap:wrap; row-gap:6px; }
|
|
403
408
|
:root[data-ofm-engine-theme] {
|
|
404
409
|
--ofm-bg:#f4f4f6; --ofm-card:#fff; --ofm-fg:#1c1c21; --ofm-quiet:#63636b;
|
|
405
410
|
--ofm-muted:#85858d; --ofm-line:#e3e3e8; --ofm-accent:#4f46e5;
|
|
@@ -762,7 +767,31 @@ export function createMagpieEngine({ dataDir, logger, binary } = {}) {
|
|
|
762
767
|
const env = { ...process.env }
|
|
763
768
|
// An inherited Magpie override must never redirect this owned instance.
|
|
764
769
|
for (const key of Object.keys(env)) if (key.toUpperCase().startsWith('MAGPIE_')) delete env[key]
|
|
770
|
+
// The engine wires every agent it finds under the user's home (ZCode's
|
|
771
|
+
// provider_config.json, Codex's config.toml, …) to itself when it starts.
|
|
772
|
+
// This engine's catalog is its own and may be empty, so that startup sync
|
|
773
|
+
// would rewrite those agents with no models. Give it a home of its own:
|
|
774
|
+
// it still serves its page, and no file of a real agent is touched. The
|
|
775
|
+
// Windows folders os.UserConfigDir reads are moved too, for the agents
|
|
776
|
+
// that keep their settings there (VS Code, Cline, …).
|
|
777
|
+
const agentHome = path.join(home, 'agent-home')
|
|
778
|
+
const agentRoaming = path.join(agentHome, 'AppData', 'Roaming')
|
|
779
|
+
const agentLocal = path.join(agentHome, 'AppData', 'Local')
|
|
780
|
+
for (const dir of [agentHome, agentRoaming, agentLocal]) {
|
|
781
|
+
await mkdir(dir, { recursive: true, mode: 0o700 })
|
|
782
|
+
}
|
|
783
|
+
// Windows folds every ancestor of a session folder into a 260-character
|
|
784
|
+
// budget; this redirection adds ~40, and claude appends a slug of the
|
|
785
|
+
// whole project path on top. A long or non-ASCII data home is the class
|
|
786
|
+
// of failure behind "rename the folder to something short and it works"
|
|
787
|
+
// — say so up front, with the escape hatch, instead of leaving a bare
|
|
788
|
+
// mkdir error nobody can act on.
|
|
789
|
+
if (agentHome.length > 200 || /[^\x20-\x7E]/.test(agentHome)) {
|
|
790
|
+
logger?.warn?.(`our-free-model: 引擎数据目录较长或含非 ASCII 字符(${agentHome.length} 字符)——Claude 等客户端可能无法新建会话文件夹;如遇到,请把 DSH_HOME 指向一个短的纯英文路径后重启。(engine data home is long or contains non-ASCII characters; if agents cannot create session folders, point DSH_HOME at a short ASCII path and restart)`)
|
|
791
|
+
}
|
|
765
792
|
Object.assign(env, {
|
|
793
|
+
HOME: agentHome, USERPROFILE: agentHome,
|
|
794
|
+
APPDATA: agentRoaming, LOCALAPPDATA: agentLocal,
|
|
766
795
|
XDG_CONFIG_HOME: dirs.config, XDG_CACHE_HOME: dirs.cache,
|
|
767
796
|
XDG_DATA_HOME: dirs.data, XDG_STATE_HOME: dirs.state,
|
|
768
797
|
MAGPIE_ADDR: `127.0.0.1:${gatewayPort}`, MAGPIE_WEB_KEY: webKey,
|
|
@@ -924,7 +953,13 @@ export function createMagpieEngine({ dataDir, logger, binary } = {}) {
|
|
|
924
953
|
if (failure.statusCode === 400) throw engineError('Invalid gateway key request', 400)
|
|
925
954
|
if (failure.statusCode === 413) throw engineError('Gateway key request exceeds limit', 413)
|
|
926
955
|
if (options.signal?.aborted) throw new DOMException('Engine request aborted', 'AbortError')
|
|
927
|
-
|
|
956
|
+
// The engine's own transport refusals carry the actionable fact (the
|
|
957
|
+
// refused path, the socket error); a bare "Engine request failed" turns a
|
|
958
|
+
// diagnosable boot into a support thread. Keep the detail, bounded, and
|
|
959
|
+
// never from response bodies — those can quote user content.
|
|
960
|
+
const detail = typeof failure?.message === 'string' && failure.message !== ''
|
|
961
|
+
? `: ${failure.message.slice(0, 160)}` : ''
|
|
962
|
+
throw engineError(`Engine request failed${detail}`)
|
|
928
963
|
}
|
|
929
964
|
}
|
|
930
965
|
const proxy = async (req, res, subpath = '/', options = {}) => {
|
|
@@ -1101,3 +1136,27 @@ export function createMagpieEngine({ dataDir, logger, binary } = {}) {
|
|
|
1101
1136
|
}
|
|
1102
1137
|
return { start, status, close, request, proxy, gateway }
|
|
1103
1138
|
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* A lightweight PATH probe for the agent CLIs the engine connects to (claude
|
|
1142
|
+
* first among them). The engine manages agent installs itself, but its
|
|
1143
|
+
* downloader cannot reach the release host from every network, so the practical
|
|
1144
|
+
* requirement is a pre-installed CLI — warn at boot instead of at the user's
|
|
1145
|
+
* first failed connect. Never resolves shell shims deeper than a PATH entry.
|
|
1146
|
+
*/
|
|
1147
|
+
export function findAgentCli(command) {
|
|
1148
|
+
const name = String(command ?? '').trim()
|
|
1149
|
+
if (name === '' || name.includes('/') || name.includes('\\')) return undefined
|
|
1150
|
+
const spellings = process.platform === 'win32' ? ['.cmd', '.bat', '.exe', ''] : ['']
|
|
1151
|
+
const dirs = [
|
|
1152
|
+
...(process.env.PATH ?? '').split(path.delimiter).filter(Boolean),
|
|
1153
|
+
...(process.platform === 'win32' && process.env.APPDATA ? [path.join(process.env.APPDATA, 'npm')] : []),
|
|
1154
|
+
]
|
|
1155
|
+
for (const dir of dirs) {
|
|
1156
|
+
for (const spelling of spellings) {
|
|
1157
|
+
const candidate = path.join(dir, `${name}${spelling}`)
|
|
1158
|
+
try { if (statSync(candidate).isFile()) return candidate } catch { /* try the next spelling */ }
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return undefined
|
|
1162
|
+
}
|
package/src/magpie-models.js
CHANGED
|
@@ -4,14 +4,42 @@ import { deniedModel } from './magpie-policy.js'
|
|
|
4
4
|
|
|
5
5
|
const PREFIX = 'subscription:magpie/'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Engine inference rides the host's replaceable `globalThis.fetch` — the same
|
|
9
|
+
* transport the imported sidecar subscriptions already use — so local proxy
|
|
10
|
+
* plugins that swap `globalThis.fetch` to re-serialize outgoing bodies can see
|
|
11
|
+
* engine turns again, as they could before the 2026-10-24 private-transport
|
|
12
|
+
* quarantine. Startup, health, management and catalog reads keep the private
|
|
13
|
+
* loopback transport: a broken fetch replacement must never keep the workspace
|
|
14
|
+
* from becoming ready. An explicit `fetchImpl` (tests, embedded hosts) still
|
|
15
|
+
* governs both paths, and OFM_ENGINE_PRIVATE_FETCH=1 forces inference back
|
|
16
|
+
* onto the private transport.
|
|
17
|
+
*/
|
|
18
|
+
function defaultInferenceTransport() {
|
|
19
|
+
return process.env.OFM_ENGINE_PRIVATE_FETCH === '1' ? loopbackFetch : globalThis.fetch
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
/** Expose the engine's live catalog through the existing DSH stream adapter. */
|
|
8
|
-
export function createMagpieModels({ engine, stats, logger, onChanged = () => {}, fetchImpl = loopbackFetch }) {
|
|
23
|
+
export function createMagpieModels({ engine, stats, logger, onChanged = () => {}, fetchImpl = loopbackFetch, inferenceFetch, extensionRuntime } = {}) {
|
|
9
24
|
let models = []
|
|
10
25
|
let pending
|
|
11
26
|
let closed = false
|
|
12
27
|
let owner
|
|
28
|
+
// An explicit fetchImpl governs inference and catalog alike (the historical
|
|
29
|
+
// contract); otherwise inference is the replaceable transport and only the
|
|
30
|
+
// catalog read stays private.
|
|
31
|
+
const catalogFetch = fetchImpl
|
|
32
|
+
const inferenceTransport = inferenceFetch ?? (fetchImpl !== loopbackFetch ? fetchImpl : defaultInferenceTransport())
|
|
13
33
|
const runtime = createSubscriptionRuntime({
|
|
14
|
-
stats, logger,
|
|
34
|
+
stats, logger,
|
|
35
|
+
fetchImpl: inferenceTransport,
|
|
36
|
+
// Enhancement extensions get the same fail-open body seam the harness lane
|
|
37
|
+
// gives them: one plain-object pass, microseconds, no gateway hop.
|
|
38
|
+
onPayload: async (payload, meta) => {
|
|
39
|
+
const source = typeof extensionRuntime === 'function' ? extensionRuntime() : extensionRuntime
|
|
40
|
+
if (source?.applyRequestHooks === undefined) return payload
|
|
41
|
+
return source.applyRequestHooks(payload, { ...meta, lane: 'magpie' })
|
|
42
|
+
},
|
|
15
43
|
subscriptions: {
|
|
16
44
|
list: () => [{
|
|
17
45
|
id: 'magpie', provider: 'magpie', enabled: engine.status().state === 'ready',
|
|
@@ -65,7 +93,7 @@ export function createMagpieModels({ engine, stats, logger, onChanged = () => {}
|
|
|
65
93
|
if (pending) return pending
|
|
66
94
|
pending = (async () => {
|
|
67
95
|
const gateway = engine.gateway()
|
|
68
|
-
const response = await
|
|
96
|
+
const response = await catalogFetch(`${gateway.url.replace(/\/$/, '')}/v1/models`, {
|
|
69
97
|
headers: { authorization: `Bearer ${gateway.key}` },
|
|
70
98
|
signal: AbortSignal.timeout(15000), redirect: 'error',
|
|
71
99
|
})
|
package/src/model-test.js
CHANGED
|
@@ -84,7 +84,9 @@ export function modelAvailability(id, snapshot, listed = false) {
|
|
|
84
84
|
// Older saved snapshots have only `at`, which retains their previous order.
|
|
85
85
|
const measured = manual && (!probe || manual.at >= (probe.startedAt ?? probe.at)) ? manual : probe
|
|
86
86
|
return {
|
|
87
|
-
availabilitySource: measured === manual && manual ? 'manual'
|
|
87
|
+
availabilitySource: measured === manual && manual ? 'manual'
|
|
88
|
+
: measured?.source === 'passive' ? 'passive'
|
|
89
|
+
: listed ? 'listing' : 'probe',
|
|
88
90
|
availability: measured?.state ?? 'unknown',
|
|
89
91
|
detail: measured?.detail ?? '', probedAt: measured?.at ?? 0,
|
|
90
92
|
ttftMs: measured?.ttftMs, latencyMs: measured?.latencyMs,
|