dsh-plugin-discover 0.0.0-stage → 1.0.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/CHANGELOG.md +31 -0
- package/LICENSE +26 -0
- package/README.md +209 -3
- package/README.zh.md +208 -0
- package/client.js +477 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +14 -0
- package/index.js +347 -0
- package/lib/bm25.js +190 -0
- package/lib/build-worker.js +32 -0
- package/lib/build.js +145 -0
- package/lib/cli.js +495 -0
- package/lib/compat.js +157 -0
- package/lib/filters.js +105 -0
- package/lib/model-client.js +163 -0
- package/lib/normalize.js +369 -0
- package/lib/paths.js +49 -0
- package/lib/quality.js +61 -0
- package/lib/readme-clean.js +128 -0
- package/lib/readme-source.js +380 -0
- package/lib/report.js +244 -0
- package/lib/runtime.js +247 -0
- package/lib/search.js +447 -0
- package/lib/store.js +184 -0
- package/lib/tokenize.js +141 -0
- package/lib/vector-store.js +142 -0
- package/lib/vector.js +592 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +67 -6
package/lib/compat.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal semantic-version comparison and DSH runtime detection. The catalog
|
|
3
|
+
* stores ordinary npm ranges; anything this parser does not understand is
|
|
4
|
+
* reported as `unknown`, which the balanced preset deliberately lets through.
|
|
5
|
+
*/
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
import { readFileSync } from 'node:fs'
|
|
8
|
+
import { dirname } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
12
|
+
|
|
13
|
+
/** Parse a semantic version; null when malformed. */
|
|
14
|
+
export function parseVersion(value) {
|
|
15
|
+
if (typeof value !== 'string') return null
|
|
16
|
+
const match = VERSION_RE.exec(value.trim())
|
|
17
|
+
if (!match) return null
|
|
18
|
+
const prerelease = match[4] ? match[4].split('.') : []
|
|
19
|
+
if (prerelease.some(part => part === '')) return null
|
|
20
|
+
return {
|
|
21
|
+
major: Number(match[1]),
|
|
22
|
+
minor: Number(match[2]),
|
|
23
|
+
patch: Number(match[3]),
|
|
24
|
+
prerelease,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compareIdentifier(a, b) {
|
|
29
|
+
const aNumeric = /^\d+$/.test(a)
|
|
30
|
+
const bNumeric = /^\d+$/.test(b)
|
|
31
|
+
if (aNumeric && bNumeric) return Number(a) - Number(b)
|
|
32
|
+
if (aNumeric) return -1
|
|
33
|
+
if (bNumeric) return 1
|
|
34
|
+
return a < b ? -1 : a > b ? 1 : 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Compare two semantic versions: <0, 0, or >0. Malformed input sorts first. */
|
|
38
|
+
export function compareVersions(a, b) {
|
|
39
|
+
const left = parseVersion(a)
|
|
40
|
+
const right = parseVersion(b)
|
|
41
|
+
if (!left && !right) return 0
|
|
42
|
+
if (!left) return -1
|
|
43
|
+
if (!right) return 1
|
|
44
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
45
|
+
if (left[key] !== right[key]) return left[key] - right[key]
|
|
46
|
+
}
|
|
47
|
+
if (left.prerelease.length === 0 && right.prerelease.length === 0) return 0
|
|
48
|
+
if (left.prerelease.length === 0) return 1
|
|
49
|
+
if (right.prerelease.length === 0) return -1
|
|
50
|
+
const length = Math.max(left.prerelease.length, right.prerelease.length)
|
|
51
|
+
for (let index = 0; index < length; index += 1) {
|
|
52
|
+
const l = left.prerelease[index]
|
|
53
|
+
const r = right.prerelease[index]
|
|
54
|
+
if (l === undefined) return -1
|
|
55
|
+
if (r === undefined) return 1
|
|
56
|
+
const compared = compareIdentifier(l, r)
|
|
57
|
+
if (compared !== 0) return compared
|
|
58
|
+
}
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function upperBound(version, kind) {
|
|
63
|
+
const parsed = parseVersion(version)
|
|
64
|
+
if (!parsed) return null
|
|
65
|
+
const { major, minor, patch } = parsed
|
|
66
|
+
if (kind === '^') {
|
|
67
|
+
if (major > 0) return `${major + 1}.0.0-0`
|
|
68
|
+
if (minor > 0) return `0.${minor + 1}.0-0`
|
|
69
|
+
return `0.0.${patch + 1}-0`
|
|
70
|
+
}
|
|
71
|
+
if (kind === '~') return `${major}.${minor + 1}.0-0`
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function satisfiesComparator(version, comparator) {
|
|
76
|
+
const match = /^(\^|~|>=|<=|>|<|=)?\s*(.+)$/.exec(comparator.trim())
|
|
77
|
+
if (!match) return null
|
|
78
|
+
const operator = match[1] ?? '='
|
|
79
|
+
const target = match[2].trim()
|
|
80
|
+
if (target === '' || target === '*' || target === 'x' || target === 'X') return true
|
|
81
|
+
if (!parseVersion(target)) return null
|
|
82
|
+
if (operator === '=') return compareVersions(version, target) === 0
|
|
83
|
+
if (operator === '>') return compareVersions(version, target) > 0
|
|
84
|
+
if (operator === '>=') return compareVersions(version, target) >= 0
|
|
85
|
+
if (operator === '<') return compareVersions(version, target) < 0
|
|
86
|
+
if (operator === '<=') return compareVersions(version, target) <= 0
|
|
87
|
+
const upper = upperBound(target, operator)
|
|
88
|
+
if (!upper) return null
|
|
89
|
+
return compareVersions(version, target) >= 0 && compareVersions(version, upper) < 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check a version against a range. Returns `true`, `false`, or `null` for a
|
|
94
|
+
* range this parser does not understand.
|
|
95
|
+
*/
|
|
96
|
+
export function satisfiesRange(version, range) {
|
|
97
|
+
if (typeof version !== 'string' || typeof range !== 'string') return null
|
|
98
|
+
if (!parseVersion(version)) return null
|
|
99
|
+
const groups = range.split('||').map(group => group.trim()).filter(Boolean)
|
|
100
|
+
if (groups.length === 0) return null
|
|
101
|
+
let understood = false
|
|
102
|
+
for (const group of groups) {
|
|
103
|
+
const comparators = group.split(/\s+/).filter(Boolean)
|
|
104
|
+
let all = true
|
|
105
|
+
let anyUnknown = false
|
|
106
|
+
for (const comparator of comparators) {
|
|
107
|
+
const result = satisfiesComparator(version, comparator)
|
|
108
|
+
if (result === null) { anyUnknown = true; all = false; break }
|
|
109
|
+
understood = true
|
|
110
|
+
if (!result) { all = false; break }
|
|
111
|
+
}
|
|
112
|
+
if (all && !anyUnknown) return true
|
|
113
|
+
}
|
|
114
|
+
return understood ? false : null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let detected
|
|
118
|
+
|
|
119
|
+
/** Guess the running DSH version without importing Host packages. */
|
|
120
|
+
export function detectRuntimeVersion() {
|
|
121
|
+
if (detected !== undefined) return detected
|
|
122
|
+
if (process.env.DSH_VERSION && parseVersion(process.env.DSH_VERSION)) {
|
|
123
|
+
detected = process.env.DSH_VERSION
|
|
124
|
+
return detected
|
|
125
|
+
}
|
|
126
|
+
const roots = new Set()
|
|
127
|
+
const add = value => { if (typeof value === 'string' && value.trim() !== '') roots.add(value) }
|
|
128
|
+
add(process.cwd())
|
|
129
|
+
add(process.env.DSH_PROFILE_DIR)
|
|
130
|
+
add(process.env.DSH_HOME)
|
|
131
|
+
try { add(dirname(fileURLToPath(import.meta.url))) } catch {}
|
|
132
|
+
const require = createRequire(import.meta.url)
|
|
133
|
+
const manifests = [
|
|
134
|
+
'@deepseek-ai/dsh-app-boot/package.json',
|
|
135
|
+
'@deepseek-ai/dsh/package.json',
|
|
136
|
+
'@deepseek-ai/dsh-cli/package.json',
|
|
137
|
+
]
|
|
138
|
+
for (const root of roots) {
|
|
139
|
+
for (const manifest of manifests) {
|
|
140
|
+
try {
|
|
141
|
+
const file = require.resolve(manifest, { paths: [root] })
|
|
142
|
+
const value = JSON.parse(readFileSync(file, 'utf8'))
|
|
143
|
+
if (value && parseVersion(value.version)) {
|
|
144
|
+
detected = value.version
|
|
145
|
+
return detected
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
detected = null
|
|
151
|
+
return detected
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Reset the detection cache (tests only). */
|
|
155
|
+
export function resetRuntimeVersionCache() {
|
|
156
|
+
detected = undefined
|
|
157
|
+
}
|
package/lib/filters.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard filtering by security, verification, and DSH compatibility. Missing
|
|
3
|
+
* fields are "unknown", not "failed": they pass and stay annotated.
|
|
4
|
+
*/
|
|
5
|
+
import { satisfiesRange } from './compat.js'
|
|
6
|
+
|
|
7
|
+
export const PRESETS = ['strict', 'balanced', 'loose', 'off']
|
|
8
|
+
export const DEFAULT_PRESET = 'balanced'
|
|
9
|
+
|
|
10
|
+
function isAudited(record) {
|
|
11
|
+
return record?.security?.status === 'audited'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function riskOf(record) {
|
|
15
|
+
return record?.security?.riskLevel ?? 'unknown'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Security gate for one preset. Unknown security never fails a preset. */
|
|
19
|
+
export function securityAllowed(record, preset) {
|
|
20
|
+
if (preset === 'strict') return isAudited(record) && riskOf(record) === 'low'
|
|
21
|
+
if (preset === 'off') return !(isAudited(record) && riskOf(record) === 'critical')
|
|
22
|
+
if (preset === 'loose') {
|
|
23
|
+
const risk = riskOf(record)
|
|
24
|
+
return !(isAudited(record) && (risk === 'high' || risk === 'critical'))
|
|
25
|
+
}
|
|
26
|
+
if (!isAudited(record)) return true
|
|
27
|
+
const risk = riskOf(record)
|
|
28
|
+
return risk !== 'high' && risk !== 'critical'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Verification gate. `0` disables the level floor for the balanced preset. */
|
|
32
|
+
export function verificationAllowed(record, preset, minVerificationLevel = 4) {
|
|
33
|
+
if (preset === 'strict') return record?.verification?.level === 5
|
|
34
|
+
if (preset !== 'balanced') return true
|
|
35
|
+
const minimum = Number.isFinite(minVerificationLevel) ? minVerificationLevel : 4
|
|
36
|
+
if (minimum <= 0) return true
|
|
37
|
+
const level = record?.verification?.level
|
|
38
|
+
return typeof level !== 'number' ? true : level >= minimum
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Compatibility state: true, false, or null for unknown. */
|
|
42
|
+
export function compatibilityOf(record, runtimeVersion) {
|
|
43
|
+
const range = record?.dshRange
|
|
44
|
+
if (typeof range !== 'string' || range.trim() === '') return null
|
|
45
|
+
if (typeof runtimeVersion !== 'string' || runtimeVersion === '') return null
|
|
46
|
+
return satisfiesRange(runtimeVersion, range)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Compatibility gate: balanced admits unknown, strict does not. */
|
|
50
|
+
export function compatibilityAllowed(record, preset, runtimeVersion) {
|
|
51
|
+
if (preset === 'strict') return compatibilityOf(record, runtimeVersion) === true
|
|
52
|
+
if (preset === 'loose' || preset === 'off') return true
|
|
53
|
+
return compatibilityOf(record, runtimeVersion) !== false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Evaluate one record against one preset. */
|
|
57
|
+
export function evaluateRecord(record, { preset = DEFAULT_PRESET, minVerificationLevel = 4, runtimeVersion = null } = {}) {
|
|
58
|
+
if (!securityAllowed(record, preset)) return { ok: false, droppedBy: 'security' }
|
|
59
|
+
if (!verificationAllowed(record, preset, minVerificationLevel)) return { ok: false, droppedBy: 'verification' }
|
|
60
|
+
if (!compatibilityAllowed(record, preset, runtimeVersion)) return { ok: false, droppedBy: 'compat' }
|
|
61
|
+
return { ok: true }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Apply one preset to every record. */
|
|
65
|
+
export function filterRecords(records, options = {}) {
|
|
66
|
+
const { preset = DEFAULT_PRESET, minVerificationLevel = 4, runtimeVersion = null } = options
|
|
67
|
+
const allowed = new Uint8Array(records.length)
|
|
68
|
+
const droppedBy = { security: 0, compatibility: 0, verification: 0 }
|
|
69
|
+
let kept = 0
|
|
70
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
71
|
+
const result = evaluateRecord(records[index], { preset, minVerificationLevel, runtimeVersion })
|
|
72
|
+
if (result.ok) {
|
|
73
|
+
allowed[index] = 1
|
|
74
|
+
kept += 1
|
|
75
|
+
} else if (result.droppedBy === 'compat') {
|
|
76
|
+
droppedBy.compatibility += 1
|
|
77
|
+
} else {
|
|
78
|
+
droppedBy[result.droppedBy] += 1
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { allowed, kept, droppedBy }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Apply the requested preset, relaxing one step at a time until at least three
|
|
86
|
+
* candidates survive. The caller reports `relaxedFrom` when this happens.
|
|
87
|
+
*/
|
|
88
|
+
export function selectFilter(records, options = {}) {
|
|
89
|
+
const { preset = DEFAULT_PRESET, minVerificationLevel = 4, runtimeVersion = null, minCandidates = 3 } = options
|
|
90
|
+
const start = PRESETS.includes(preset) ? preset : DEFAULT_PRESET
|
|
91
|
+
const startIndex = PRESETS.indexOf(start)
|
|
92
|
+
for (let index = startIndex; index < PRESETS.length; index += 1) {
|
|
93
|
+
const candidate = PRESETS[index]
|
|
94
|
+
const result = filterRecords(records, { preset: candidate, minVerificationLevel, runtimeVersion })
|
|
95
|
+
if (result.kept >= minCandidates || candidate === 'off') {
|
|
96
|
+
return {
|
|
97
|
+
preset: candidate,
|
|
98
|
+
relaxedFrom: candidate === start ? null : start,
|
|
99
|
+
...result,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const fallback = filterRecords(records, { preset: 'off', minVerificationLevel, runtimeVersion })
|
|
104
|
+
return { preset: 'off', relaxedFrom: start, ...fallback }
|
|
105
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the optional model package's line-delimited JSON server. The
|
|
3
|
+
* server owns the ~500 MB ONNX/tokenizer RSS; the Host only holds vectors.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { createInterface } from 'node:readline'
|
|
10
|
+
|
|
11
|
+
/** The optional model package is not installed or its server is missing. */
|
|
12
|
+
export class ModelUnavailableError extends Error {
|
|
13
|
+
constructor(message = 'model package is not installed') {
|
|
14
|
+
super(message)
|
|
15
|
+
this.name = 'ModelUnavailableError'
|
|
16
|
+
this.code = 'MODEL_UNAVAILABLE'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Resolve the server entry through the index pointer, then the profile. */
|
|
21
|
+
export function resolveModelServer(indexDir) {
|
|
22
|
+
try {
|
|
23
|
+
const pointer = JSON.parse(readFileSync(join(indexDir, 'model.json'), 'utf8'))
|
|
24
|
+
if (pointer && typeof pointer.server === 'string' && existsSync(pointer.server)) return pointer.server
|
|
25
|
+
} catch {}
|
|
26
|
+
const profileDir = process.env.DSH_PROFILE_DIR
|
|
27
|
+
if (profileDir) {
|
|
28
|
+
try {
|
|
29
|
+
const require = createRequire(join(profileDir, 'package.json'))
|
|
30
|
+
const server = require.resolve('dsh-plugin-discover-model/server.js')
|
|
31
|
+
if (existsSync(server)) return server
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Spawned model server with request/response correlation and idle respawn. */
|
|
38
|
+
export class ModelClient {
|
|
39
|
+
constructor(serverPath, { timeoutMs = 60_000 } = {}) {
|
|
40
|
+
this.serverPath = serverPath
|
|
41
|
+
this.timeoutMs = timeoutMs
|
|
42
|
+
this.child = null
|
|
43
|
+
this.pending = new Map()
|
|
44
|
+
this.nextId = 1
|
|
45
|
+
this.stderr = ''
|
|
46
|
+
this.closed = false
|
|
47
|
+
this.embeddedDocs = 0
|
|
48
|
+
// Recycling is an escape hatch for pathological environments, not the
|
|
49
|
+
// default: the model is capped at four ONNX threads instead of all cores.
|
|
50
|
+
this.recycleDocs = Number(process.env.DSH_MODEL_RECYCLE_DOCS) || 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Stop this child after a bounded number of documents; the next request respawns. */
|
|
54
|
+
recycle() {
|
|
55
|
+
this.failAll(new ModelUnavailableError('model client recycled'))
|
|
56
|
+
if (this.child) {
|
|
57
|
+
try { this.child.kill() } catch {}
|
|
58
|
+
this.child = null
|
|
59
|
+
}
|
|
60
|
+
this.embeddedDocs = 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
start() {
|
|
64
|
+
if (this.child) return
|
|
65
|
+
if (this.closed) throw new ModelUnavailableError('model client is disposed')
|
|
66
|
+
const env = { ...process.env }
|
|
67
|
+
if (process.versions.electron) env.ELECTRON_RUN_AS_NODE = '1'
|
|
68
|
+
const command = process.env.DSH_PLUGIN_DISCOVER_NODE || process.execPath
|
|
69
|
+
let child
|
|
70
|
+
try {
|
|
71
|
+
child = spawn(command, [this.serverPath], {
|
|
72
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
73
|
+
windowsHide: true,
|
|
74
|
+
env,
|
|
75
|
+
})
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new ModelUnavailableError(`cannot start model server: ${error.message}`)
|
|
78
|
+
}
|
|
79
|
+
this.child = child
|
|
80
|
+
createInterface({ input: child.stdout, crlfDelay: Infinity }).on('line', line => {
|
|
81
|
+
const text = line.trim()
|
|
82
|
+
if (text === '') return
|
|
83
|
+
let response
|
|
84
|
+
try {
|
|
85
|
+
response = JSON.parse(text)
|
|
86
|
+
} catch {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
const pending = this.pending.get(response.id)
|
|
90
|
+
if (!pending) return
|
|
91
|
+
this.pending.delete(response.id)
|
|
92
|
+
clearTimeout(pending.timer)
|
|
93
|
+
if (typeof response.error === 'string') pending.reject(new Error(response.error))
|
|
94
|
+
else pending.resolve(response)
|
|
95
|
+
})
|
|
96
|
+
child.stderr.on('data', chunk => {
|
|
97
|
+
this.stderr = `${this.stderr}${chunk.toString()}`.slice(-4000)
|
|
98
|
+
})
|
|
99
|
+
child.on('error', error => {
|
|
100
|
+
if (this.child !== child) return
|
|
101
|
+
this.failAll(new ModelUnavailableError(`model server error: ${error.message}`))
|
|
102
|
+
this.child = null
|
|
103
|
+
})
|
|
104
|
+
child.on('exit', code => {
|
|
105
|
+
if (this.child !== child) return
|
|
106
|
+
this.failAll(new ModelUnavailableError(
|
|
107
|
+
`model server exited (${code})${this.stderr ? `: ${this.stderr.slice(-500)}` : ''}`,
|
|
108
|
+
))
|
|
109
|
+
this.child = null
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
failAll(error) {
|
|
114
|
+
for (const [id, pending] of this.pending) {
|
|
115
|
+
clearTimeout(pending.timer)
|
|
116
|
+
pending.reject(error)
|
|
117
|
+
this.pending.delete(id)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
request(op, payload, timeoutMs = this.timeoutMs) {
|
|
122
|
+
this.start()
|
|
123
|
+
const id = this.nextId++
|
|
124
|
+
const line = `${JSON.stringify({ id, op, ...payload })}\n`
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const timer = setTimeout(() => {
|
|
127
|
+
this.pending.delete(id)
|
|
128
|
+
reject(new Error(`model server request timed out after ${timeoutMs} ms`))
|
|
129
|
+
}, timeoutMs)
|
|
130
|
+
this.pending.set(id, { resolve, reject, timer })
|
|
131
|
+
try {
|
|
132
|
+
this.child.stdin.write(line)
|
|
133
|
+
} catch (error) {
|
|
134
|
+
clearTimeout(timer)
|
|
135
|
+
this.pending.delete(id)
|
|
136
|
+
reject(new ModelUnavailableError(`cannot write to model server: ${error.message}`))
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async embed(texts, kind = 'passage') {
|
|
142
|
+
if (this.recycleDocs > 0 && this.embeddedDocs >= this.recycleDocs) this.recycle()
|
|
143
|
+
const response = await this.request('embed', { texts, kind }, Math.max(this.timeoutMs, 120_000))
|
|
144
|
+
if (!Array.isArray(response.vectors)) throw new Error('model server returned no vectors')
|
|
145
|
+
this.embeddedDocs += texts.length
|
|
146
|
+
if (this.recycleDocs > 0 && this.embeddedDocs >= this.recycleDocs) this.recycle()
|
|
147
|
+
return response.vectors.map(vector => Float32Array.from(vector))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async truncate(text, maxTokens = 384) {
|
|
151
|
+
const response = await this.request('truncate', { text, maxTokens })
|
|
152
|
+
return { text: String(response.text ?? ''), tokens: Number(response.tokens ?? 0) }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
dispose() {
|
|
156
|
+
this.closed = true
|
|
157
|
+
this.failAll(new ModelUnavailableError('model client disposed'))
|
|
158
|
+
if (this.child) {
|
|
159
|
+
try { this.child.kill() } catch {}
|
|
160
|
+
this.child = null
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|