@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/cordis.patch.yml
CHANGED
package/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import fs from 'node:fs'
|
|
|
29
29
|
import crypto from 'node:crypto'
|
|
30
30
|
import { fileURLToPath } from 'node:url'
|
|
31
31
|
import { externalUrl, openExternal } from './src/open-external.js'
|
|
32
|
+
import { findBunExecutable } from './src/bun-fetch.js'
|
|
32
33
|
import { ROUTE_LABELS, ROUTE_MAIN, ROUTE_REGION } from './src/adapter.js'
|
|
33
34
|
import { DATA_DIR_NAME, resolveDshHome, publicModelVisibility, validateModelVisibility, isModelHidden } from './src/store.js'
|
|
34
35
|
import { isEacEntry, isKiloEntry } from './src/catalog.js'
|
|
@@ -36,7 +37,7 @@ import { STATE } from './src/probe.js'
|
|
|
36
37
|
import { generateKey, rankLanAddresses, startForwardServer, startLanRelay } from './src/forward.js'
|
|
37
38
|
import { chanGatewayCredential, chanGatewayEnabled, chanGatewayPort, startChanRelay } from './src/chan-relay.js'
|
|
38
39
|
import { CODE, UpstreamError } from './src/http.js'
|
|
39
|
-
import { egressActive, egressFetch, outletLabel, readOutletSelection, startEgressRelay } from './src/egress.js'
|
|
40
|
+
import { egressActive, egressFetch, outletLabel, readOutletSelection, startEgressRelay, setFreeLaneProxy, freeLaneProxyActive, laneProxyEcho } from './src/egress.js'
|
|
40
41
|
import { directFetch } from './src/eac.js'
|
|
41
42
|
import { clearEacUser, readEacUser, writeEacUser } from './src/eac-user.js'
|
|
42
43
|
import { createEacLoginPoller } from './src/eac-login.js'
|
|
@@ -56,16 +57,18 @@ import { getProvider, listProviders } from './src/providers/index.js'
|
|
|
56
57
|
import { addSavings, describeSavingsTotal, emptySavings, saverConfig, savingsPercent } from './src/token-saver.js'
|
|
57
58
|
import { CAVEMAN_LEVELS, PONYTAIL_LEVELS } from './src/token-saver.js'
|
|
58
59
|
import { OPTIMIZATIONS } from './src/local-optimize.js'
|
|
59
|
-
import { createModelRuntime, createRuntimeStores } from './src/core
|
|
60
|
+
import { createModelRuntime, createRuntimeStores } from './src/core.js'
|
|
60
61
|
import { createSubscriptions } from './src/subscriptions.js'
|
|
61
62
|
import { createSubscriptionRuntime } from './src/subscription-completion.js'
|
|
62
63
|
import { isSubscriptionEntry } from './src/subscription-adapter.js'
|
|
63
|
-
import { createMagpieEngine } from './src/magpie-engine.js'
|
|
64
|
+
import { createMagpieEngine, findAgentCli } from './src/magpie-engine.js'
|
|
65
|
+
import { createExtensionRuntime } from './src/extensions.js'
|
|
66
|
+
import { builtinDescriptors } from './src/plugins.js'
|
|
64
67
|
import { createMagpieModels, combineSubscriptionRuntimes } from './src/magpie-models.js'
|
|
65
68
|
import { DISCLAIMER, DISCLAIMER_VERSION, disclaimerAcknowledged } from './src/disclaimer.js'
|
|
66
69
|
|
|
67
|
-
export { fromOpenAiMessages } from './src/core
|
|
68
|
-
import { buildStats } from './src/core
|
|
70
|
+
export { fromOpenAiMessages } from './src/core.js'
|
|
71
|
+
import { buildStats } from './src/core.js'
|
|
69
72
|
export { buildStats }
|
|
70
73
|
|
|
71
74
|
export const name = 'our-free-model'
|
|
@@ -145,6 +148,15 @@ export function apply(ctx, config) {
|
|
|
145
148
|
fs.mkdirSync(dataDir, { recursive: true })
|
|
146
149
|
const packageVersion = version
|
|
147
150
|
|
|
151
|
+
// Environment readiness: the official free lane's POST inference needs a
|
|
152
|
+
// local Bun, and a user who never learns that reads only a failed turn.
|
|
153
|
+
// Probe once at boot — the summary's `bun` bit and this log line are the two
|
|
154
|
+
// surfaces the answer lives on.
|
|
155
|
+
const bunAvailable = findBunExecutable() !== undefined
|
|
156
|
+
if (!bunAvailable) {
|
|
157
|
+
logger.warn?.('our-free-model: 未检测到本机 Bun —— OpenCode 免费车道无法推理。请安装 Bun(https://bun.sh 或 npm i -g bun)后重启,或把可执行文件路径写入 OUR_FREE_MODEL_BUN。(Bun not found: the OpenCode free lane cannot run inference)')
|
|
158
|
+
}
|
|
159
|
+
|
|
148
160
|
// A hot reload re-enters apply with fresh stores; the generation counter lives
|
|
149
161
|
// on globalThis so the new instance knows it replaced a predecessor, and does
|
|
150
162
|
// the post-swap bookkeeping itself (the old closure must not touch stores
|
|
@@ -414,9 +426,13 @@ export function apply(ctx, config) {
|
|
|
414
426
|
}
|
|
415
427
|
|
|
416
428
|
const magpieEngine = createMagpieEngine({ dataDir, logger })
|
|
429
|
+
// The extension runtime is created a few lines below; the lazy reference
|
|
430
|
+
// gives the magpie lane the enhancement-hook seam without reordering boot.
|
|
431
|
+
let extensionHookRuntime = null
|
|
417
432
|
const magpieModels = createMagpieModels({
|
|
418
433
|
engine: magpieEngine, stats, logger,
|
|
419
434
|
onChanged: () => modelRuntime.syncAggregateCatalog(),
|
|
435
|
+
extensionRuntime: () => extensionHookRuntime,
|
|
420
436
|
})
|
|
421
437
|
let subscriptions
|
|
422
438
|
let subscriptionRuntime
|
|
@@ -429,17 +445,35 @@ export function apply(ctx, config) {
|
|
|
429
445
|
logger.warn?.('our-free-model: private subscription storage is unavailable')
|
|
430
446
|
}
|
|
431
447
|
subscriptionRuntime = combineSubscriptionRuntimes(magpieModels, subscriptionRuntime)
|
|
448
|
+
// The OFM extension platform: the marketplace's client side. Created before
|
|
449
|
+
// the model runtime so the extension lane exists for the first merge; the
|
|
450
|
+
// async mount (worker starts, registry warm-up) lands through the callback.
|
|
451
|
+
const extensions = createExtensionRuntime({
|
|
452
|
+
dataDir,
|
|
453
|
+
logger,
|
|
454
|
+
hostVersion: packageVersion,
|
|
455
|
+
onCatalogChanged: () => {
|
|
456
|
+
if (disposed) return
|
|
457
|
+
try { modelRuntime.syncExtensionCatalog() } catch { /* the runtime may not exist yet during boot */ }
|
|
458
|
+
},
|
|
459
|
+
})
|
|
460
|
+
// Built-ins register through the same kernel as installed plugins. The DSH
|
|
461
|
+
// client renders channel/enhancement contributions; page contributions are
|
|
462
|
+
// a desktop-console surface.
|
|
463
|
+
for (const descriptor of builtinDescriptors()) extensions.registerBuiltin(descriptor)
|
|
464
|
+
extensionHookRuntime = extensions
|
|
432
465
|
const modelRuntime = createModelRuntime({
|
|
433
466
|
...stores,
|
|
434
467
|
logger,
|
|
435
468
|
subscriptionRuntime,
|
|
469
|
+
extensionRuntime: extensions,
|
|
436
470
|
attributionUserAgent: 'deepseek-harness',
|
|
437
471
|
resolveImage: imageResolver(ctx, logger),
|
|
438
472
|
sealedCredential: sealedCredentialOf,
|
|
439
473
|
onSealedUnavailable: () => logger.warn?.('our-free-model: the sealed lane is not available in this composition (no recognized host profile); its models stay hidden'),
|
|
440
474
|
onTopology: () => emitTopology(),
|
|
441
475
|
})
|
|
442
|
-
const { state, adapter, refreshCatalog, refreshAvailability, watchEgress, fetchListing, publicModelRows, syncAggregateCatalog } = modelRuntime
|
|
476
|
+
const { state, adapter, refreshCatalog, refreshAvailability, watchEgress, fetchListing, publicModelRows, syncAggregateCatalog, clearLaneBreaker } = modelRuntime
|
|
443
477
|
const runForwarded = modelRuntime.complete
|
|
444
478
|
|
|
445
479
|
// ── registration ────────────────────────────────────────────────────────────
|
|
@@ -857,6 +891,23 @@ export function apply(ctx, config) {
|
|
|
857
891
|
outletSyncInFlight = run
|
|
858
892
|
try { await run } finally { if (outletSyncInFlight === run) outletSyncInFlight = null }
|
|
859
893
|
}
|
|
894
|
+
/**
|
|
895
|
+
* Apply the settings file's lane proxy to the egress layer.
|
|
896
|
+
*
|
|
897
|
+
* Boot-time sync; the settings route re-applies on every `freeLane` patch
|
|
898
|
+
* through `sanitizeSettings` (which is where a bad address fails the write).
|
|
899
|
+
* A stored value that no longer parses is dropped with one log line — the
|
|
900
|
+
* lane falls back to the global outlet rather than failing every turn.
|
|
901
|
+
*/
|
|
902
|
+
function syncFreeLaneProxy() {
|
|
903
|
+
try {
|
|
904
|
+
setFreeLaneProxy(String(settings.get().freeLane?.proxyUrl ?? ''))
|
|
905
|
+
} catch (error) {
|
|
906
|
+
logger.warn?.(`our-free-model: the OpenCode lane proxy address is invalid (${error?.message ?? error}); the lane keeps its current exit`)
|
|
907
|
+
setFreeLaneProxy('')
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
syncFreeLaneProxy()
|
|
860
911
|
async function syncEgressOnce() {
|
|
861
912
|
const desired = settings.get().egress ?? {}
|
|
862
913
|
const wanted = desired.enabled === true
|
|
@@ -1086,7 +1137,9 @@ export function apply(ctx, config) {
|
|
|
1086
1137
|
}
|
|
1087
1138
|
const api = createApiRoutes({
|
|
1088
1139
|
settings, stats, availability, catalog: () => modelRuntime.catalog, state,
|
|
1089
|
-
refreshCatalog, refreshAvailability, syncForward, syncRelay, syncEgress,
|
|
1140
|
+
refreshCatalog, refreshAvailability, syncForward, syncRelay, syncEgress, clearLaneBreaker,
|
|
1141
|
+
extensions,
|
|
1142
|
+
bunAvailable,
|
|
1090
1143
|
onVisibilityChanged: emitTopology,
|
|
1091
1144
|
pool: fetchPoolSnapshot,
|
|
1092
1145
|
eacAuth,
|
|
@@ -1440,6 +1493,7 @@ export function apply(ctx, config) {
|
|
|
1440
1493
|
|
|
1441
1494
|
// ── boot + background loop ──────────────────────────────────────────────────
|
|
1442
1495
|
ctx.effect(() => () => {
|
|
1496
|
+
extensions.dispose()
|
|
1443
1497
|
magpieModels.close()
|
|
1444
1498
|
const closing = magpieEngine.close()
|
|
1445
1499
|
modelRuntime.dispose()
|
|
@@ -1452,9 +1506,40 @@ export function apply(ctx, config) {
|
|
|
1452
1506
|
void magpieEngine.start().then(() => magpieModels.refresh()).catch(() => {
|
|
1453
1507
|
logger.warn?.('our-free-model: model workspace could not resume')
|
|
1454
1508
|
})
|
|
1509
|
+
// The engine connects Claude through the locally installed Claude Code
|
|
1510
|
+
// CLI; its own downloader cannot reach the release host from every
|
|
1511
|
+
// network, so a pre-installed CLI is the practical requirement. Warn at
|
|
1512
|
+
// boot — the user's first failed connect is too late to learn it.
|
|
1513
|
+
if (findAgentCli('claude') === undefined) {
|
|
1514
|
+
logger.warn?.('our-free-model: 未检测到 Claude Code CLI —— 在模型工作台连接 Claude 前请先安装(npm i -g @anthropic-ai/claude-code)。(Claude Code CLI not found; install it before connecting Claude in the model workspace)')
|
|
1515
|
+
}
|
|
1455
1516
|
}
|
|
1456
1517
|
}, 'our-free-model: model workspace')
|
|
1457
1518
|
|
|
1519
|
+
ctx.effect(() => {
|
|
1520
|
+
// The extension mount: start every installed, enabled extension (its
|
|
1521
|
+
// catalog lands through onCatalogChanged → syncExtensionCatalog), then
|
|
1522
|
+
// warm the marketplace cache in the background — a cold registry only
|
|
1523
|
+
// means an empty Discover tab, never a blocked lane.
|
|
1524
|
+
void extensions.init().catch(error => {
|
|
1525
|
+
logger.warn?.(`our-free-model: extension mount failed (${error?.message ?? error})`)
|
|
1526
|
+
})
|
|
1527
|
+
void extensions.refreshRegistry().catch(() => {})
|
|
1528
|
+
// The marketplace index refreshes on its own cadence too: an update that
|
|
1529
|
+
// lands upstream should reach the Installed tab without the user opening
|
|
1530
|
+
// the market first. The TTL cache keeps ticks cheap; failures are silent —
|
|
1531
|
+
// the next tick retries and the last good view keeps serving.
|
|
1532
|
+
const registryTimer = setInterval(() => {
|
|
1533
|
+
void extensions.refreshRegistry().catch(() => {})
|
|
1534
|
+
// The automatic updater rides the same tick: it only installs what the
|
|
1535
|
+
// soak has already cleared, so no extra network touch beyond the
|
|
1536
|
+
// refresh's own fetches, and a failure is next tick's retry.
|
|
1537
|
+
if (settings.get().autoUpdateExtensions === true) void extensions.autoUpdate().catch(() => {})
|
|
1538
|
+
}, 6 * 60 * 60 * 1000)
|
|
1539
|
+
registryTimer.unref?.()
|
|
1540
|
+
return () => clearInterval(registryTimer)
|
|
1541
|
+
}, 'our-free-model: extensions')
|
|
1542
|
+
|
|
1458
1543
|
ctx.effect(() => () => {
|
|
1459
1544
|
void forward?.close().catch(() => {})
|
|
1460
1545
|
void relay?.close().catch(() => {})
|
|
@@ -1566,7 +1651,9 @@ export function apply(ctx, config) {
|
|
|
1566
1651
|
every(() => {
|
|
1567
1652
|
void (async () => {
|
|
1568
1653
|
await watchEgress()
|
|
1569
|
-
|
|
1654
|
+
// throttleListing: the periodic round may keep the cached roster when it
|
|
1655
|
+
// just refreshed — the upstream listing is read at most once per window.
|
|
1656
|
+
await refreshCatalog({ probe: false, throttleListing: true })
|
|
1570
1657
|
})().catch(error => logger.warn?.(`our-free-model: periodic refresh failed (${error?.message ?? error})`))
|
|
1571
1658
|
}, () => positiveOr(settings.get().probeIntervalMinutes, 15, 1) * 60_000)
|
|
1572
1659
|
// A failing egress watch means the network is down, which is worth one line —
|
|
@@ -1663,6 +1750,16 @@ function sanitizeSettings(patch, current) {
|
|
|
1663
1750
|
const hours = Number(next.updateCheckHours)
|
|
1664
1751
|
next.updateCheckHours = Number.isFinite(hours) && hours >= 0 ? Math.trunc(hours) : (Number(current.updateCheckHours) || 6)
|
|
1665
1752
|
}
|
|
1753
|
+
if (next.freeLane !== undefined) {
|
|
1754
|
+
// The lane proxy is a URL the egress layer validates on save: a typo must
|
|
1755
|
+
// fail the write, not the first turn. Everything else about the object is
|
|
1756
|
+
// carried verbatim.
|
|
1757
|
+
const incoming = next.freeLane
|
|
1758
|
+
const merged = { ...(current.freeLane ?? {}), ...(incoming && typeof incoming === 'object' ? incoming : {}) }
|
|
1759
|
+
const proxyUrl = String(merged.proxyUrl ?? '').trim()
|
|
1760
|
+
setFreeLaneProxy(proxyUrl)
|
|
1761
|
+
next.freeLane = { ...merged, proxyUrl }
|
|
1762
|
+
}
|
|
1666
1763
|
return next
|
|
1667
1764
|
}
|
|
1668
1765
|
|
|
@@ -1987,7 +2084,7 @@ export function createApiRoutes(deps) {
|
|
|
1987
2084
|
const patch = await readJson(req)
|
|
1988
2085
|
const visibility = validateModelVisibility(patch)
|
|
1989
2086
|
const current = deps.settings.get()
|
|
1990
|
-
const next = sanitizeSettings({ ...current, ...pick(patch, ['enabled', 'exposeRegionModels', 'probeIntervalMinutes', 'defaultMaxTokens', 'announcementAck', 'feedUrl', 'feedPollMinutes', 'notifyOs', 'updateCheckHours', 'autoReloadWatch']), ...visibility }, current)
|
|
2087
|
+
const next = sanitizeSettings({ ...current, ...pick(patch, ['enabled', 'exposeRegionModels', 'probeIntervalMinutes', 'defaultMaxTokens', 'announcementAck', 'feedUrl', 'feedPollMinutes', 'notifyOs', 'updateCheckHours', 'autoReloadWatch', 'freeLane', 'developerPlugins', 'autoUpdateExtensions']), ...visibility }, current)
|
|
1991
2088
|
if (next.autoReloadWatch === true && deps.update.status().hotReloadAvailable === false) next.autoReloadWatch = false
|
|
1992
2089
|
if (patch.forward !== undefined) {
|
|
1993
2090
|
const forward = { ...(current.forward ?? {}), ...pick(patch.forward, ['enabled', 'host', 'port']) }
|
|
@@ -2053,8 +2150,134 @@ export function createApiRoutes(deps) {
|
|
|
2053
2150
|
return send(200, { ok: true, ...buildSummary(deps) })
|
|
2054
2151
|
}
|
|
2055
2152
|
if (method === 'POST' && routePath === '/reprobe') {
|
|
2056
|
-
|
|
2057
|
-
|
|
2153
|
+
// One probe, one model: the caller's pick, or the roster head. The
|
|
2154
|
+
// quota breaker refuses while a window is open (the response names it).
|
|
2155
|
+
const body = await readJson(req).catch(() => ({}))
|
|
2156
|
+
const outcome = await deps.refreshAvailability(true, { model: typeof body?.model === 'string' ? body.model : undefined })
|
|
2157
|
+
return send(200, { ok: true, probe: outcome, ...buildSummary(deps) })
|
|
2158
|
+
}
|
|
2159
|
+
if (method === 'POST' && routePath === '/lane/clear') {
|
|
2160
|
+
// 换出口后重试一次:出口已变(或用户自备了新出口代理),把额度断路器
|
|
2161
|
+
// 清掉,让下一次手动检测/真实回合重新出发。一次调用只清一次。
|
|
2162
|
+
const cleared = deps.clearLaneBreaker()
|
|
2163
|
+
return send(200, { ok: true, cleared, ...buildSummary(deps) })
|
|
2164
|
+
}
|
|
2165
|
+
if (method === 'POST' && routePath === '/lane/echo') {
|
|
2166
|
+
// 测试出口:打第三方回显服务,不打免费网关 —— 一次测试不花配额。
|
|
2167
|
+
try {
|
|
2168
|
+
const echo = await laneProxyEcho({ signal: req.signal ?? undefined })
|
|
2169
|
+
return send(200, { ok: true, ...echo })
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
return send(502, { ok: false, error: String(error?.message ?? error).slice(0, 200) })
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// ── the extension marketplace ───────────────────────────────────────────
|
|
2176
|
+
if (routePath === '/extensions' || routePath.startsWith('/extensions/')) {
|
|
2177
|
+
if (deps.extensions === undefined) return send(503, { error: 'the extension platform is unavailable' })
|
|
2178
|
+
const extensions = deps.extensions
|
|
2179
|
+
if (method === 'GET' && routePath === '/extensions/cover') {
|
|
2180
|
+
// The marketplace card's banner. An <img> cannot attach headers, so
|
|
2181
|
+
// the image rides the same admission fence as every fetch from the
|
|
2182
|
+
// settings page — same origin, same cookies, same gate. Failure is
|
|
2183
|
+
// a 404 the card renders without; a missing banner never breaks it.
|
|
2184
|
+
const image = await extensions.coverImage(url.searchParams.get('id') ?? '')
|
|
2185
|
+
if (image === null) return send(404, { error: 'no cover for this extension' })
|
|
2186
|
+
res.writeHead(200, {
|
|
2187
|
+
'content-type': image.contentType,
|
|
2188
|
+
'content-length': image.bytes.length,
|
|
2189
|
+
// Cosmetics only: a stale banner for a few minutes is invisible.
|
|
2190
|
+
'cache-control': 'private, max-age=600',
|
|
2191
|
+
'x-content-type-options': 'nosniff',
|
|
2192
|
+
})
|
|
2193
|
+
res.end(image.bytes)
|
|
2194
|
+
return
|
|
2195
|
+
}
|
|
2196
|
+
if (method === 'GET' && routePath === '/extensions') {
|
|
2197
|
+
// A first paint waits briefly for the boot warm-up and hears why a
|
|
2198
|
+
// read failed; `registryError` used to be structurally empty because
|
|
2199
|
+
// `registryView()` never throws.
|
|
2200
|
+
const status = await extensions.registryStatusSoon()
|
|
2201
|
+
// `updates` feeds the soak countdown on the Installed tab; `audit`
|
|
2202
|
+
// gives the page its own 最近操作 trail; the settings bit lets the
|
|
2203
|
+
// toolbar's auto-update switch render the value it will write.
|
|
2204
|
+
return send(200, {
|
|
2205
|
+
installed: extensions.listInstalled().filter(record => record.builtin !== true),
|
|
2206
|
+
registry: status.registry,
|
|
2207
|
+
registryError: status.error,
|
|
2208
|
+
updates: extensions.updatesAvailable(),
|
|
2209
|
+
registryMeta: extensions.registryMeta(),
|
|
2210
|
+
audit: extensions.auditTail(30),
|
|
2211
|
+
autoUpdate: deps.settings.get().autoUpdateExtensions === true,
|
|
2212
|
+
})
|
|
2213
|
+
}
|
|
2214
|
+
if (method === 'POST' && routePath === '/extensions/registry/refresh') {
|
|
2215
|
+
try {
|
|
2216
|
+
await extensions.refreshRegistry({ force: true })
|
|
2217
|
+
return send(200, { ok: true, installed: extensions.listInstalled().filter(record => record.builtin !== true), registry: extensions.registryView() })
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
return send(502, { error: String(error?.message ?? error), installed: extensions.listInstalled().filter(record => record.builtin !== true) })
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
if (method === 'POST' && routePath === '/extensions/install') {
|
|
2223
|
+
const body = await readJson(req)
|
|
2224
|
+
try {
|
|
2225
|
+
const record = await extensions.install(String(body?.id ?? ''), { version: typeof body?.version === 'string' ? body.version : undefined })
|
|
2226
|
+
return send(200, { ok: true, record, installed: extensions.listInstalled().filter(record => record.builtin !== true) })
|
|
2227
|
+
} catch (error) {
|
|
2228
|
+
return send(400, { error: String(error?.message ?? error) })
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
if (method === 'POST' && routePath === '/extensions/uninstall') {
|
|
2232
|
+
const body = await readJson(req)
|
|
2233
|
+
try {
|
|
2234
|
+
await extensions.uninstall(String(body?.id ?? ''))
|
|
2235
|
+
return send(200, { ok: true, installed: extensions.listInstalled().filter(record => record.builtin !== true) })
|
|
2236
|
+
} catch (error) {
|
|
2237
|
+
return send(400, { error: String(error?.message ?? error) })
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
if (method === 'POST' && routePath === '/extensions/toggle') {
|
|
2241
|
+
const body = await readJson(req)
|
|
2242
|
+
try {
|
|
2243
|
+
const record = await extensions.setEnabled(String(body?.id ?? ''), body?.enabled === true)
|
|
2244
|
+
return send(200, { ok: true, record, installed: extensions.listInstalled().filter(record => record.builtin !== true) })
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
return send(400, { error: String(error?.message ?? error) })
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
if (method === 'POST' && routePath === '/extensions/config') {
|
|
2250
|
+
const body = await readJson(req)
|
|
2251
|
+
try {
|
|
2252
|
+
const record = await extensions.saveConfig(String(body?.id ?? ''), body?.values ?? {})
|
|
2253
|
+
// `installed` travels with every mutation: the page renders its cards
|
|
2254
|
+
// from it, so a config write has to hand the new values back or the
|
|
2255
|
+
// control snaps to the state it was rendered with.
|
|
2256
|
+
return send(200, { ok: true, record, installed: extensions.listInstalled().filter(record => record.builtin !== true) })
|
|
2257
|
+
} catch (error) {
|
|
2258
|
+
return send(400, { error: String(error?.message ?? error) })
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
if (method === 'POST' && routePath === '/extensions/secret') {
|
|
2262
|
+
const body = await readJson(req)
|
|
2263
|
+
try {
|
|
2264
|
+
const record = await extensions.saveSecret(String(body?.id ?? ''), String(body?.key ?? ''), body?.value)
|
|
2265
|
+
return send(200, { ok: true, record, installed: extensions.listInstalled().filter(record => record.builtin !== true) })
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
return send(400, { error: String(error?.message ?? error) })
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
if (method === 'POST' && routePath === '/extensions/self-test') {
|
|
2271
|
+
// The manifest's optional selfTest(ctx) on demand; the verdict rides
|
|
2272
|
+
// back as data (a failed hook is a result, not an HTTP error), so the
|
|
2273
|
+
// card renders ✗ + reason instead of the page falling into the toast.
|
|
2274
|
+
const body = await readJson(req)
|
|
2275
|
+
try {
|
|
2276
|
+
return send(200, await extensions.runSelfTest(String(body?.id ?? '')))
|
|
2277
|
+
} catch (error) {
|
|
2278
|
+
return send(400, { error: String(error?.message ?? error) })
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2058
2281
|
}
|
|
2059
2282
|
if (method === 'GET' && routePath === '/forward/key') {
|
|
2060
2283
|
return send(200, { key: deps.settings.get().forwardKey ?? '' })
|
|
@@ -2469,6 +2692,19 @@ function buildSummary(deps) {
|
|
|
2469
2692
|
settings: publicSettings(deps.settings.get(), forwardInfo, deps.egressInfo()),
|
|
2470
2693
|
egress: forwardInfo.egress ?? snapshot.egress ?? null,
|
|
2471
2694
|
outlet: deps.egressInfo(),
|
|
2695
|
+
// The free lane's quota breaker and its exit: what the page's lane card
|
|
2696
|
+
// renders as the countdown / lock banner, and the proxy seam issue #708's
|
|
2697
|
+
// bypass reduces to. `lane` is null until a real verdict exists.
|
|
2698
|
+
lane: snapshot.lane ?? null,
|
|
2699
|
+
freeLane: {
|
|
2700
|
+
proxyActive: freeLaneProxyActive(),
|
|
2701
|
+
proxyConfigured: String(deps.settings.get().freeLane?.proxyUrl ?? '').trim() !== '',
|
|
2702
|
+
transport: process.env.OUR_FREE_MODEL_FREE_TRANSPORT ?? 'auto',
|
|
2703
|
+
},
|
|
2704
|
+
// Environment readiness for the free lane: the official POST inference
|
|
2705
|
+
// needs a local Bun, and a user who never learns that reads only a failed
|
|
2706
|
+
// turn. Probed once at boot (see the readiness effect below).
|
|
2707
|
+
bun: deps.bunAvailable !== false,
|
|
2472
2708
|
probedAt: snapshot.at ?? 0,
|
|
2473
2709
|
announcementVersion: ANNOUNCEMENT_VERSION,
|
|
2474
2710
|
disclaimerVersion: DISCLAIMER_VERSION,
|
|
@@ -2486,6 +2722,7 @@ function buildSummary(deps) {
|
|
|
2486
2722
|
// Whether the absorbed free-channel pack is live on this host, and its
|
|
2487
2723
|
// failure reason when it is not (see the mount in `apply`).
|
|
2488
2724
|
channels: deps.channels?.() ?? { state: 'unknown', error: '' },
|
|
2725
|
+
extensions: deps.extensions !== undefined ? { installed: deps.extensions.listInstalled().filter(record => record.builtin !== true) } : null,
|
|
2489
2726
|
update: { available: update.available, latest: update.latest, current: update.current, checkedAt: update.checkedAt, applying: update.applying, managed: update.managed === true },
|
|
2490
2727
|
}
|
|
2491
2728
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ebony-vinyl/dsh-our-free-model",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Our Free Model — 在 dsh 里装上这个插件即可,无需登录、注册或填 API Key,就能使用包括 DeepSeek V4.1 Flash、Kimi K3 在内的前沿模型,桌面端另有 EAC 协付渠道;2.0 起接入 LobsterAI、Qoder、TRAE、Cline、Loomy、Raccoon、MiniMax、ZCode、Gemini 十个账号渠道(原有匿名免费通道保留)(登录、账号池、积分领取、本地 OpenAI 网关与局域网转发、数据看板与运行日志),六页毛玻璃界面统一呈现。",
|
|
6
6
|
"repository": {
|
|
@@ -58,7 +58,10 @@
|
|
|
58
58
|
"src/push.js",
|
|
59
59
|
"src/recovery.js",
|
|
60
60
|
"src/reload.js",
|
|
61
|
+
"src/semver.js",
|
|
61
62
|
"src/store.js",
|
|
63
|
+
"src/extensions.js",
|
|
64
|
+
"src/plugins.js",
|
|
62
65
|
"src/stream.js",
|
|
63
66
|
"src/subscription-adapter.js",
|
|
64
67
|
"src/subscription-completion.js",
|
|
@@ -71,9 +74,10 @@
|
|
|
71
74
|
"src/vault.js",
|
|
72
75
|
"src/token-saver.js",
|
|
73
76
|
"src/engine-parts",
|
|
74
|
-
"src/core",
|
|
77
|
+
"src/core.js",
|
|
75
78
|
"src/providers",
|
|
76
79
|
"src/route",
|
|
80
|
+
"src/o2a.js",
|
|
77
81
|
"vendor/channel-pack/pack.js",
|
|
78
82
|
"vendor/channel-pack/qoder-auth-wasm.wasm",
|
|
79
83
|
"vendor/channel-pack/NOTICE.md",
|
package/src/adapter.js
CHANGED
|
@@ -22,12 +22,12 @@ import { toChatMessages, toClaudeMessages, toResponseInput, toToolDefs, repairTo
|
|
|
22
22
|
import { CODE, UpstreamError, postStreamed } from './http.js'
|
|
23
23
|
import { postSealedStreamed } from './eac.js'
|
|
24
24
|
import { postKiloStreamed } from './kilo.js'
|
|
25
|
-
import { postAggregateStreamed, isAggregateEntry } from './aggregate.js'
|
|
25
|
+
import { postAggregateStreamed, postCompatibleStreamed, isAggregateEntry } from './aggregate.js'
|
|
26
26
|
import { finishReason, readStream, sanitizeUsage, windowTokens } from './stream.js'
|
|
27
27
|
import { DEFAULT_LEVEL, MIN_BUDGET, budgetFor, defaultEffortFor, effortPatchFor, effortsFor, hasDeclaredEffortMenu, resolveLevel } from './effort.js'
|
|
28
28
|
import { createChannel } from './channel.js'
|
|
29
29
|
import { recoveryPolicy, canRecover, canRecoverSilentStop, recoveryMessages, continuationMessages, checkpointFits, addUsage, createBlockTracker } from './recovery.js'
|
|
30
|
-
import { isEacEntry, isKiloEntry } from './catalog.js'
|
|
30
|
+
import { isEacEntry, isKiloEntry, isExtensionEntry } from './catalog.js'
|
|
31
31
|
import { isComboEntry, comboChain } from './combos.js'
|
|
32
32
|
import { applyTokenSavers, saverConfig } from './token-saver.js'
|
|
33
33
|
import { classifyFailure, describeVerdict, shouldAdvance } from './route/classify.js'
|
|
@@ -375,6 +375,25 @@ export class FreeModelAdapter {
|
|
|
375
375
|
const sealed = isEacEntry(entry)
|
|
376
376
|
const kilo = isKiloEntry(entry)
|
|
377
377
|
const aggregate = isAggregateEntry(entry)
|
|
378
|
+
const extension = isExtensionEntry(entry)
|
|
379
|
+
// The keyless anonymous lane — the only one the quota breaker and the
|
|
380
|
+
// passive availability learner describe. Every other lane carries its own
|
|
381
|
+
// channel tag and its own accounting.
|
|
382
|
+
const freeLane = !sealed && !kilo && !aggregate && !extension && !isComboEntry(entry) && !isSubscriptionEntry(entry)
|
|
383
|
+
if (freeLane) {
|
|
384
|
+
// The quota breaker: a window the upstream opened on this egress is a
|
|
385
|
+
// wall no model/session/UA choice moves (issue #767 measured exactly
|
|
386
|
+
// that), so the lane refuses locally instead of feeding the wall another
|
|
387
|
+
// request. `重试` here means changing the exit or waiting for `retryAt`.
|
|
388
|
+
const gate = this.deps.freeLaneGate?.()
|
|
389
|
+
if (gate?.ok === false) {
|
|
390
|
+
const when = Number.isFinite(gate.retryAt) ? new Date(gate.retryAt).toISOString() : 'until the exit changes'
|
|
391
|
+
throw new UpstreamError(
|
|
392
|
+
`the OpenCode free quota window is open on this egress${gate.message ? `: ${gate.message}` : ''} — retry after ${when}, or switch the lane's exit proxy; Kilo and extension channels are unaffected`,
|
|
393
|
+
CODE.quota,
|
|
394
|
+
{ ...(Number.isFinite(gate.retryAt) ? { providerRetryAfterMs: Math.max(1000, gate.retryAt - Date.now()) } : {}), laneQuota: true, laneState: gate.state })
|
|
395
|
+
}
|
|
396
|
+
}
|
|
378
397
|
// The absorbed channels speak the Chat wire regardless of what their ids
|
|
379
398
|
// resemble; only the free lane splits endpoints per model, and an aggregate
|
|
380
399
|
// row carries the wire its provider actually serves.
|
|
@@ -386,7 +405,10 @@ export class FreeModelAdapter {
|
|
|
386
405
|
const budget = budgetFor(options.reasoningEffort, entry, options.maxTokens, settings.defaultMaxTokens)
|
|
387
406
|
const declared = toToolDefs(options.tools, style)
|
|
388
407
|
const policy = recoveryPolicy(settings.streamRecovery)
|
|
389
|
-
const
|
|
408
|
+
const sessionSeed = typeof options.purpose === 'string' && options.purpose.trim() !== ''
|
|
409
|
+
? `${options.purpose.trim()}:${options.sessionId ?? 'global'}`
|
|
410
|
+
: options.sessionId
|
|
411
|
+
const session = sessionForConversation(sessionSeed)
|
|
390
412
|
// A turn without a conversation id lands on the reserved "global" session,
|
|
391
413
|
// shared by every such turn on this machine. Minting a fresh session per
|
|
392
414
|
// id-less turn is not an option: the free lane accounts quota per session,
|
|
@@ -492,13 +514,20 @@ export class FreeModelAdapter {
|
|
|
492
514
|
const attemptStarted = Date.now()
|
|
493
515
|
const recovering = attempt === 1
|
|
494
516
|
const savingsSink = {}
|
|
495
|
-
|
|
517
|
+
let payload = payloadFor(attemptMessages, attemptBudget, recovering ? [] : warnings, savingsSink)
|
|
496
518
|
attemptSavings = savingsSink.report ?? null
|
|
519
|
+
// Enhancement extensions see the harness body after every lane's own
|
|
520
|
+
// shaping but before the free lane's fingerprint — they rewrite the
|
|
521
|
+
// caller's request, not the camouflage. Fail-open inside the runtime.
|
|
522
|
+
if (this.deps.extensionRuntime?.applyRequestHooks !== undefined) {
|
|
523
|
+
payload = await this.deps.extensionRuntime.applyRequestHooks(payload, { model: entry.id, wire, sessionId: session })
|
|
524
|
+
}
|
|
497
525
|
// The absorbed channels have no tool-name gate; their models see the
|
|
498
526
|
// caller's tools exactly as declared, so the fingerprint pass is free-lane
|
|
499
527
|
// only. The aggregate lane is somebody else's gateway entirely — renaming
|
|
500
528
|
// its tools to this lane's gate placeholders would break the caller's calls.
|
|
501
|
-
|
|
529
|
+
// An extension channel shaped its own body in its sandbox.
|
|
530
|
+
const renameMap = sealed || kilo || aggregate || extension ? new Map() : applyFingerprint(payload, wire === 'messages' ? 'claude' : style === 'flat')
|
|
502
531
|
const controller = new AbortController()
|
|
503
532
|
const onAbort = () => controller.abort(options.signal?.reason)
|
|
504
533
|
options.signal?.addEventListener('abort', onAbort, { once: true })
|
|
@@ -525,7 +554,14 @@ export class FreeModelAdapter {
|
|
|
525
554
|
signal: controller.signal,
|
|
526
555
|
onData: value => channel.push(value),
|
|
527
556
|
})
|
|
528
|
-
:
|
|
557
|
+
: extension
|
|
558
|
+
? this.deps.extensionRuntime.prepareChat(entry, payload, session)
|
|
559
|
+
.then(prepared => postCompatibleStreamed({
|
|
560
|
+
...prepared,
|
|
561
|
+
signal: controller.signal,
|
|
562
|
+
onData: value => channel.push(value),
|
|
563
|
+
}))
|
|
564
|
+
: postStreamed({
|
|
529
565
|
path: endpointFor(entry.id), body: payload, session,
|
|
530
566
|
attributionUserAgent: snapshot.attributionUserAgent,
|
|
531
567
|
signal: controller.signal,
|
|
@@ -544,6 +580,10 @@ export class FreeModelAdapter {
|
|
|
544
580
|
if (recorded) return
|
|
545
581
|
recorded = true
|
|
546
582
|
const usage = outcome?.sawUsage === true ? outcome.usage : undefined
|
|
583
|
+
this.deps.extensionRuntime?.noteUsage?.({
|
|
584
|
+
at: attemptStarted, model: entry.id, extensionId: entry.extensionId, ok,
|
|
585
|
+
input: usage?.inputTokens ?? 0, output: usage?.outputTokens ?? 0,
|
|
586
|
+
})
|
|
547
587
|
this.deps.recordUsage({
|
|
548
588
|
at: attemptStarted,
|
|
549
589
|
model: entry.id,
|
|
@@ -689,6 +729,7 @@ export class FreeModelAdapter {
|
|
|
689
729
|
return { ok: false, ...advanceVerdict(CODE.empty, 'empty response') }
|
|
690
730
|
}
|
|
691
731
|
record(true, outcome, recovering ? { recovered: reason.kind === 'stop' } : {})
|
|
732
|
+
if (freeLane) this.deps.onFreeLaneOutcome?.({ entry, ok: true })
|
|
692
733
|
finishTurn(true, recovering && reason.kind === 'stop', attempt + 1)
|
|
693
734
|
yield { type: 'usage', usage: totalUsage }
|
|
694
735
|
// Keep the provider token when the harness fallback maps an abnormal
|
|
@@ -698,6 +739,12 @@ export class FreeModelAdapter {
|
|
|
698
739
|
return { advance: false, verdict: null, ok: true, recovered: recovering && reason.kind === 'stop' }
|
|
699
740
|
} catch (error) {
|
|
700
741
|
if (error?.code === CODE.region) this.deps.onRegionBlocked?.(entry.id)
|
|
742
|
+
// Passive availability: a real turn's verdict is the only probe this
|
|
743
|
+
// lane still spends. A 429 opens the quota breaker; a client-gate 403
|
|
744
|
+
// locks the lane; a region refusal marks just the model.
|
|
745
|
+
if (freeLane && options.signal?.aborted !== true && error?.code !== CODE.aborted) {
|
|
746
|
+
this.deps.onFreeLaneOutcome?.({ entry, ok: false, error })
|
|
747
|
+
}
|
|
701
748
|
const aborted = options.signal?.aborted === true
|
|
702
749
|
let failure = toFailure(error)
|
|
703
750
|
if (sealed && failure.code === CODE.server) failure = { ...failure, code: 'EAC_UPSTREAM_FAILED' }
|
|
@@ -917,6 +964,7 @@ function toFailure(error) {
|
|
|
917
964
|
function wireOf(entry) {
|
|
918
965
|
if (isSubscriptionEntry(entry)) return entry.wire ?? 'chat'
|
|
919
966
|
if (isAggregateEntry(entry)) return entry.wire ?? 'chat'
|
|
967
|
+
if (isExtensionEntry(entry)) return entry.wire ?? 'chat'
|
|
920
968
|
if (isEacEntry(entry) || isKiloEntry(entry)) return 'chat'
|
|
921
969
|
return wireFor(entry.id)
|
|
922
970
|
}
|