@jedylicong/dsh-xinlab 0.0.0-stage → 0.2.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.
@@ -0,0 +1,99 @@
1
+ import z from '@deepseek-ai/schemastery'
2
+ import { readFileSync } from 'node:fs'
3
+ import { LlmAdapter } from '@deepseek-ai/dsh-llm'
4
+ import { rejectEmbeddedCredentials } from './catalog.mjs'
5
+ import { assertModelSettings, modelKeyRef, modelProfile, PROTOCOLS } from './model-settings.mjs'
6
+ import { officialAdapter } from './official-adapter.mjs'
7
+ import { ModelKeyPreviews } from './key-preview.mjs'
8
+ import { keyPreviewDescriptor } from './key-preview-protocol.mjs'
9
+
10
+ export const name = 'xinlab-providers'
11
+ export const inject = ['llm', 'credentials']
12
+ export const Config = z.object({
13
+ // Deployment-owned catalog is deliberately not volatile: the page edits only
14
+ // protocol choices. Secrets have no slot in this configuration schema.
15
+ providers: z.any().required(),
16
+ protocols: z.dict(z.dict(z.union(PROTOCOLS))).default({}).volatile(),
17
+ })
18
+
19
+ export class ModelSettingsAdapter extends LlmAdapter {
20
+ constructor(ctx, providers, protocols) {
21
+ super()
22
+ this.ctx = ctx
23
+ this.providers = structuredClone(providers)
24
+ this.protocols = protocols
25
+ this.base = officialAdapter(ctx, this.providers)
26
+ this.models = new Map()
27
+ }
28
+
29
+ delegate(provider, model) {
30
+ const profile = this.providers[provider]
31
+ if (!profile?.models?.some((item) => item.id === model)) throw new Error('未知供应商或模型。')
32
+ const protocol = this.protocols()[provider]?.[model] ?? profile.api
33
+ const id = JSON.stringify([provider, model])
34
+ let current = this.models.get(id)
35
+ if (!current || current.protocol !== protocol) {
36
+ const selected = modelProfile(profile, model, protocol)
37
+ const ref = modelKeyRef(provider, model)
38
+ const baseRef = selected.apiKeyEnv
39
+ selected.apiKeyEnv = ref
40
+ const next = { ...officialAdapter(this.ctx, { [provider]: selected }, { ref, baseRef }), protocol }
41
+ this.models.set(id, next)
42
+ // Prepared calls retain their official adapter snapshot; releasing the
43
+ // composition's listeners does not mutate that snapshot or its auth seam.
44
+ current?.dispose()
45
+ current = next
46
+ }
47
+ return current.adapter
48
+ }
49
+
50
+ providerInfo(provider) { return this.base.adapter.providerInfo(provider) }
51
+ providerRetryPolicy(provider) { return this.base.adapter.providerRetryPolicy(provider) }
52
+ imageRequestPricing(provider, model) { return this.delegate(provider, model).imageRequestPricing(provider, model) }
53
+ imageRequestTarget(provider, model, image) { return this.delegate(provider, model).imageRequestTarget(provider, model, image) }
54
+ async listModels(provider) {
55
+ const delegates = (this.providers[provider]?.models ?? []).map((model) => this.delegate(provider, model.id))
56
+ const result = []
57
+ // Bound concurrency on small management nodes and credential file stores.
58
+ for (const adapter of delegates) result.push(...await adapter.listModels(provider))
59
+ return result
60
+ }
61
+ resolveModel(provider, model, signal) { return this.delegate(provider, model).resolveModel(provider, model, signal) }
62
+ prepareCall(provider, model, signal) { return this.delegate(provider, model).prepareCall(provider, model, signal) }
63
+ stream(options) { return this.delegate(options.provider, options.model).stream(options) }
64
+ async dispose() {
65
+ await this.base.dispose()
66
+ for (const entry of this.models.values()) await entry.dispose()
67
+ this.models.clear()
68
+ }
69
+ }
70
+
71
+ export function apply(ctx, config) {
72
+ rejectEmbeddedCredentials(config.providers)
73
+ assertModelSettings(config.providers, config.protocols.get())
74
+ const adapter = new ModelSettingsAdapter(ctx, config.providers, () => config.protocols.get())
75
+ // Validate every saved protocol before persistence, including protocol-specific
76
+ // compatibility. A rejected draft never replaces the working configuration.
77
+ ctx.on('internal/config', (fiber, read) => {
78
+ const raw = read()
79
+ if (fiber !== ctx.fiber) return raw
80
+ const next = Config(raw)
81
+ assertModelSettings(config.providers, next.protocols.get())
82
+ for (const [provider, models] of Object.entries(next.protocols.get())) {
83
+ for (const [model, protocol] of Object.entries(models)) {
84
+ const check = officialAdapter(ctx, { [provider]: modelProfile(config.providers[provider], model, protocol) })
85
+ check.dispose()
86
+ }
87
+ }
88
+ return raw
89
+ })
90
+ ctx.llm.registerAdapter(Object.keys(config.providers), adapter)
91
+ ctx.inject(['typert'], (child) => {
92
+ new ModelKeyPreviews(child, config.providers)
93
+ const packageName = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).name
94
+ child.effect(() => child.typert.register({ package: packageName, face: 'host', schemas: [],
95
+ model: { services: [], events: [], objects: [] }, invocations: [keyPreviewDescriptor(packageName)] }))
96
+ })
97
+ ctx.inject(['settings'], (child) => child.effect(() => child.settings.configure({ auto: false }, ctx.fiber)))
98
+ ctx.effect(() => () => adapter.dispose())
99
+ }
package/src/state.mjs ADDED
@@ -0,0 +1,40 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, writeFileSync } from 'node:fs'
3
+ import { isAbsolute, join } from 'node:path'
4
+
5
+ function privateFile(path, initial, minimumBytes = 0) {
6
+ let fd
7
+ try {
8
+ try {
9
+ fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600)
10
+ writeFileSync(fd, initial)
11
+ fsyncSync(fd)
12
+ } catch (error) {
13
+ if (error.code !== 'EEXIST') throw error
14
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW)
15
+ }
16
+ const stat = fstatSync(fd)
17
+ if (!stat.isFile() || stat.size < minimumBytes) throw new Error('vLLM 状态文件类型或长度不正确')
18
+ if (process.getuid && stat.uid !== process.getuid()) throw new Error('vLLM 状态文件不属于当前运行用户')
19
+ // Enforce the same file modes as the managed deployment without rotating state.
20
+ fchmodSync(fd, 0o600)
21
+ } finally {
22
+ if (fd !== undefined) closeSync(fd)
23
+ }
24
+ }
25
+
26
+ export function ensureUUIDState(directory) {
27
+ if (typeof directory !== 'string' || !isAbsolute(directory)) throw new Error('vLLM 状态目录必须使用绝对路径')
28
+ mkdirSync(directory, { recursive: true, mode: 0o700 })
29
+ const stat = lstatSync(directory)
30
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('vLLM 状态目录必须是实际目录')
31
+ if (process.getuid && stat.uid !== process.getuid()) throw new Error('vLLM 状态目录不属于当前运行用户')
32
+ // Open the directory itself to avoid chmod following a substituted leaf symlink.
33
+ const fd = openSync(directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW)
34
+ try { fchmodSync(fd, 0o700) } finally { closeSync(fd) }
35
+ const keyFile = join(directory, 'key')
36
+ const warmFile = join(directory, 'warm-uuids')
37
+ privateFile(keyFile, randomBytes(32), 32)
38
+ privateFile(warmFile, '')
39
+ return { keyFile, warmFile }
40
+ }
@@ -0,0 +1,14 @@
1
+ import { apply as applyManagedUUID } from '../vendor/vllm-mm-uuid.mjs'
2
+ import { ensureUUIDState } from './state.mjs'
3
+
4
+ export const name = 'xinlab-vllm-mm-uuid'
5
+ export const inject = ['llm', 'attachments']
6
+
7
+ export function apply(ctx, config) {
8
+ const endpoint = new URL(config.endpoint)
9
+ if (!['http:', 'https:'].includes(endpoint.protocol) || !endpoint.pathname.endsWith('/chat/completions')) {
10
+ throw new Error('vLLM UUID 插件需要准确的 Chat Completions 地址')
11
+ }
12
+ const files = ensureUUIDState(config.stateDir)
13
+ applyManagedUUID(ctx, { ...files, endpoint: endpoint.href, providers: ['vllm'] })
14
+ }
@@ -0,0 +1,239 @@
1
+ import { createHmac } from 'node:crypto'
2
+ import { appendFileSync, readFileSync } from 'node:fs'
3
+
4
+ const CACHE_MISS = /Cache miss for image at index \d+ but data is not provided\./
5
+ const DATA_IMAGE = /^data:image\/(?:png|jpeg|webp|gif);base64,/
6
+ const ATTACHMENT_FAILURES = new Set([
7
+ 'ATTACHMENT_NOT_FOUND',
8
+ 'ATTACHMENT_CORRUPT',
9
+ 'ATTACHMENT_READ_FAILED',
10
+ ])
11
+ const RECOVERY = Symbol('vllm-mm-uuid-recovery')
12
+ const PLACEHOLDER = '[historical image unavailable in the local attachment store]'
13
+
14
+ export const name = 'vllm-mm-uuid'
15
+ export const inject = ['llm', 'attachments']
16
+
17
+ function stableUuid(key, dataUrl) {
18
+ const bytes = createHmac('sha256', key).update(dataUrl).digest().subarray(0, 16)
19
+ bytes[6] = (bytes[6] & 0x0f) | 0x40
20
+ bytes[8] = (bytes[8] & 0x3f) | 0x80
21
+ const hex = bytes.toString('hex')
22
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
23
+ }
24
+
25
+ function visitImageParts(raw, projected, key, seen, uuids) {
26
+ if (Array.isArray(raw)) {
27
+ for (let index = 0; index < raw.length; index += 1) {
28
+ visitImageParts(raw[index], projected[index], key, seen, uuids)
29
+ }
30
+ return
31
+ }
32
+ if (!raw || typeof raw !== 'object') return
33
+
34
+ const url = raw.type === 'image_url' && raw.image_url && typeof raw.image_url.url === 'string'
35
+ ? raw.image_url.url
36
+ : undefined
37
+ if (url && DATA_IMAGE.test(url)) {
38
+ const uuid = stableUuid(key, url)
39
+ raw.uuid = uuid
40
+ projected.uuid = uuid
41
+ if (seen.has(uuid)) projected.image_url = null
42
+ uuids.add(uuid)
43
+ return
44
+ }
45
+
46
+ for (const field of Object.keys(raw)) {
47
+ if (projected[field] !== undefined) visitImageParts(raw[field], projected[field], key, seen, uuids)
48
+ }
49
+ }
50
+
51
+ export function preparePayload(text, key, seen) {
52
+ const raw = JSON.parse(text)
53
+ const projected = structuredClone(raw)
54
+ const uuids = new Set()
55
+ visitImageParts(raw, projected, key, seen, uuids)
56
+ if (uuids.size === 0) return undefined
57
+ return {
58
+ projected: JSON.stringify(projected),
59
+ raw: JSON.stringify(raw),
60
+ uuids: [...uuids],
61
+ }
62
+ }
63
+
64
+ function requestUrl(input) {
65
+ return typeof input === 'string' || input instanceof URL ? String(input) : input.url
66
+ }
67
+
68
+ async function requestBody(input, init) {
69
+ if (typeof init?.body === 'string') return init.body
70
+ if (input instanceof Request) return input.clone().text()
71
+ return undefined
72
+ }
73
+
74
+ function withBody(input, init, body) {
75
+ const headers = new Headers(input instanceof Request ? input.headers : init?.headers)
76
+ headers.delete('content-length')
77
+ if (input instanceof Request) return [new Request(input, { body, headers }), undefined]
78
+ return [input, { ...init, body, headers }]
79
+ }
80
+
81
+ async function isUuidCacheMiss(response) {
82
+ if (response.status !== 400) return false
83
+ return CACHE_MISS.test(await response.clone().text())
84
+ }
85
+
86
+ export function createFetchWrapper(nativeFetch, { endpoint, key, seen, remember }) {
87
+ const expected = new URL(endpoint).href
88
+ return async function vllmUuidFetch(input, init) {
89
+ if (new URL(requestUrl(input)).href !== expected) return nativeFetch(input, init)
90
+
91
+ const text = await requestBody(input, init)
92
+ if (text === undefined) return nativeFetch(input, init)
93
+
94
+ let payload
95
+ try {
96
+ payload = preparePayload(text, key, seen)
97
+ } catch {
98
+ return nativeFetch(input, init)
99
+ }
100
+ if (payload === undefined) return nativeFetch(input, init)
101
+
102
+ let request = withBody(input, init, payload.projected)
103
+ let response = await nativeFetch(...request)
104
+ if (await isUuidCacheMiss(response)) {
105
+ request = withBody(input, init, payload.raw)
106
+ response = await nativeFetch(...request)
107
+ }
108
+ if (response.ok) remember(payload.uuids)
109
+ return response
110
+ }
111
+ }
112
+
113
+ function replaceMissingBlocks(blocks, missing) {
114
+ return blocks.map((block) => {
115
+ if (block.type === 'image' && missing.has(String(block.attachment.attachmentId))) {
116
+ return { type: 'text', text: PLACEHOLDER }
117
+ }
118
+ if (block.type === 'tool-result') return { ...block, content: replaceMissingBlocks(block.content, missing) }
119
+ return block
120
+ })
121
+ }
122
+
123
+ export function projectMissingMessages(messages, missing) {
124
+ return messages.map((message) => ({ ...message, content: replaceMissingBlocks(message.content, missing) }))
125
+ }
126
+
127
+ function collectImageRefs(blocks, refs) {
128
+ for (const block of blocks) {
129
+ if (block.type === 'image') refs.set(String(block.attachment.attachmentId), block.attachment)
130
+ if (block.type === 'tool-result') collectImageRefs(block.content, refs)
131
+ }
132
+ }
133
+
134
+ async function unavailableAttachments(ctx, options) {
135
+ const refs = new Map()
136
+ for (const message of options.messages) collectImageRefs(message.content, refs)
137
+ const missing = new Set()
138
+ await Promise.all([...refs].map(async ([id, ref]) => {
139
+ try {
140
+ await ctx.attachments.readImage(ref, options.signal)
141
+ } catch (error) {
142
+ if (options.signal?.aborted) throw error
143
+ missing.add(id)
144
+ }
145
+ }))
146
+ return missing
147
+ }
148
+
149
+ function attachmentFailure(chunk) {
150
+ return chunk.type === 'finish'
151
+ && chunk.reason.kind === 'error'
152
+ && ATTACHMENT_FAILURES.has(chunk.reason.failure.code)
153
+ }
154
+
155
+ function retryStream(ctx, options, messages) {
156
+ const retry = { ...options, messages }
157
+ Object.defineProperty(retry, RECOVERY, { value: true })
158
+ return ctx.llm.stream(retry)
159
+ }
160
+
161
+ function recoverAttachmentFailures(ctx, options, next, knownMissing) {
162
+ return (async function* () {
163
+ const alreadyMissing = new Set()
164
+ for (const message of options.messages) {
165
+ const refs = new Map()
166
+ collectImageRefs(message.content, refs)
167
+ for (const id of refs.keys()) if (knownMissing.has(id)) alreadyMissing.add(id)
168
+ }
169
+ if (alreadyMissing.size > 0) {
170
+ yield* retryStream(ctx, options, projectMissingMessages(options.messages, alreadyMissing))
171
+ return
172
+ }
173
+
174
+ let emitted = false
175
+ for await (const chunk of next()) {
176
+ if (!emitted && attachmentFailure(chunk)) {
177
+ const missing = await unavailableAttachments(ctx, options)
178
+ for (const id of missing) knownMissing.add(id)
179
+ yield* retryStream(
180
+ ctx,
181
+ options,
182
+ missing.size === 0 ? options.messages : projectMissingMessages(options.messages, missing),
183
+ )
184
+ return
185
+ }
186
+ emitted = true
187
+ yield chunk
188
+ }
189
+ })()
190
+ }
191
+
192
+ function readState(config) {
193
+ const key = readFileSync(config.keyFile)
194
+ if (key.length < 32) throw new Error('vllm-mm-uuid: keyFile must contain at least 32 bytes')
195
+ const seen = new Set()
196
+ try {
197
+ for (const line of readFileSync(config.warmFile, 'utf8').split('\n')) if (line) seen.add(line)
198
+ } catch (error) {
199
+ if (error.code !== 'ENOENT') throw error
200
+ }
201
+ return { key, seen }
202
+ }
203
+
204
+ export function apply(ctx, config) {
205
+ if (!config || typeof config.endpoint !== 'string' || !config.endpoint) {
206
+ throw new Error('vllm-mm-uuid: endpoint is required')
207
+ }
208
+ if (typeof config.keyFile !== 'string' || typeof config.warmFile !== 'string') {
209
+ throw new Error('vllm-mm-uuid: keyFile and warmFile are required')
210
+ }
211
+ const providers = new Set(config.providers ?? ['vllm'])
212
+ const { key, seen } = readState(config)
213
+ const knownMissing = new Set()
214
+ const remember = (uuids) => {
215
+ const added = uuids.filter((uuid) => !seen.has(uuid))
216
+ if (added.length === 0) return
217
+ for (const uuid of added) seen.add(uuid)
218
+ appendFileSync(config.warmFile, `${added.join('\n')}\n`, { mode: 0o600 })
219
+ }
220
+
221
+ ctx.effect(() => {
222
+ const nativeFetch = globalThis.fetch
223
+ const wrapped = createFetchWrapper(nativeFetch, {
224
+ endpoint: config.endpoint,
225
+ key,
226
+ seen,
227
+ remember,
228
+ })
229
+ globalThis.fetch = wrapped
230
+ return () => {
231
+ if (globalThis.fetch === wrapped) globalThis.fetch = nativeFetch
232
+ }
233
+ }, 'vllm-mm-uuid: fetch wrapper')
234
+
235
+ ctx.on('llm/stream', (options, next) => {
236
+ if (!providers.has(options.provider) || options[RECOVERY]) return next()
237
+ return recoverAttachmentFailures(ctx, options, next, knownMissing)
238
+ })
239
+ }
@@ -0,0 +1,41 @@
1
+ const EMPTY_RESPONSE = {
2
+ message: 'vllm returned reasoning without text or tool calls',
3
+ code: 'EMPTY_RESPONSE',
4
+ }
5
+
6
+ export const name = 'vllm-reasoning-only-retry'
7
+ export const inject = ['llm']
8
+
9
+ function hasText(value) {
10
+ return typeof value === 'string' && value.trim().length > 0
11
+ }
12
+
13
+ export async function* classifyReasoningOnly(stream) {
14
+ let reasoning = false
15
+ let output = false
16
+
17
+ for await (const chunk of stream) {
18
+ if (chunk.type === 'reasoning-delta' && hasText(chunk.text)) reasoning = true
19
+ if (chunk.type === 'text-delta' && hasText(chunk.text)) output = true
20
+ if (chunk.type === 'tool-call-delta') output = true
21
+ if (chunk.type === 'block-start' && chunk.blockType === 'tool-call') output = true
22
+ if (chunk.type === 'block-end') {
23
+ if (chunk.block.type === 'reasoning' && hasText(chunk.block.text)) reasoning = true
24
+ if (chunk.block.type === 'text' && hasText(chunk.block.text)) output = true
25
+ if (chunk.block.type === 'tool-call') output = true
26
+ }
27
+
28
+ if (chunk.type === 'finish' && chunk.reason.kind === 'stop' && reasoning && !output) {
29
+ yield { ...chunk, reason: { kind: 'error', failure: EMPTY_RESPONSE } }
30
+ } else {
31
+ yield chunk
32
+ }
33
+ }
34
+ }
35
+
36
+ export function apply(ctx) {
37
+ ctx.on('llm/stream', (options, next) => {
38
+ if (options.provider !== 'vllm') return next()
39
+ return classifyReasoningOnly(next())
40
+ })
41
+ }