@mengruo/dsh-vision-toolkit 0.0.1 → 0.1.1
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.i18n.yaml +2 -2
- package/README.md +17 -42
- package/README.zh.md +16 -41
- package/assets/logo_eapi_dark.png +0 -0
- package/docs/aihubmix-gemini-vision.i18n.yaml +2 -2
- package/docs/aihubmix-gemini-vision.md +2 -2
- package/docs/aihubmix-gemini-vision.zh.md +2 -2
- package/lib/client.js +246 -68
- package/lib/client.js.map +1 -1
- package/lib/config.js +143 -24
- package/lib/config.js.map +1 -1
- package/lib/evidence-cache.js +14 -0
- package/lib/evidence-cache.js.map +1 -1
- package/lib/image-input-variants.js +48 -24
- package/lib/image-input-variants.js.map +1 -1
- package/lib/runtime-install.js +204 -19
- package/lib/runtime-install.js.map +1 -1
- package/lib/runtime.js +310 -154
- package/lib/runtime.js.map +1 -1
- package/lib/tools.js +1 -0
- package/lib/tools.js.map +1 -1
- package/lib/types/client/index.d.ts +45 -3
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/config.d.ts +53 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/evidence-cache.d.ts.map +1 -1
- package/lib/types/image-input-variants.d.ts +9 -4
- package/lib/types/image-input-variants.d.ts.map +1 -1
- package/lib/types/runtime-install.d.ts +21 -0
- package/lib/types/runtime-install.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +45 -10
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/tools.d.ts.map +1 -1
- package/lib/types/upstream.d.ts +3 -0
- package/lib/types/upstream.d.ts.map +1 -1
- package/lib/types/web.d.ts +8 -0
- package/lib/types/web.d.ts.map +1 -1
- package/lib/upstream.js +11 -6
- package/lib/upstream.js.map +1 -1
- package/lib/web.js +50 -7
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.tsx +331 -88
- package/src/config.ts +210 -28
- package/src/evidence-cache.ts +14 -0
- package/src/image-input-variants.ts +54 -22
- package/src/runtime-install.ts +231 -20
- package/src/runtime.ts +333 -180
- package/src/tools.ts +1 -0
- package/src/upstream.ts +15 -8
- package/src/web.ts +67 -8
package/src/config.ts
CHANGED
|
@@ -44,6 +44,38 @@ const BUILT_IN_FREE_VISION_MODEL_ALIASES = new Set([
|
|
|
44
44
|
'moondream3.1-9B-A2B',
|
|
45
45
|
])
|
|
46
46
|
|
|
47
|
+
/** One online vision provider in the failover pool. */
|
|
48
|
+
export interface VisionProviderConfig {
|
|
49
|
+
/** Stable unique id used to derive the auto-generated credential name. */
|
|
50
|
+
id?: string
|
|
51
|
+
/** Display label shown in Settings; defaults to the model name. */
|
|
52
|
+
name?: string
|
|
53
|
+
/** Whether this provider participates in the failover pool (default true). */
|
|
54
|
+
enabled?: boolean
|
|
55
|
+
/** Provider API base URL. */
|
|
56
|
+
baseUrl?: string
|
|
57
|
+
/** DSH Credential reference holding the API key (an environment-style name). */
|
|
58
|
+
credential?: string
|
|
59
|
+
/** Multimodal model name. */
|
|
60
|
+
model?: string
|
|
61
|
+
/** Vision request protocol: OpenAI Chat Completions or Anthropic Messages. */
|
|
62
|
+
protocol?: 'openai' | 'anthropic'
|
|
63
|
+
/** Anthropic thinking field behavior; `omit` leaves model defaults untouched. */
|
|
64
|
+
anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
|
|
65
|
+
/** Outbound User-Agent for provider requests and connection tests. */
|
|
66
|
+
userAgent?: string
|
|
67
|
+
/** Per-provider single request timeout in milliseconds. */
|
|
68
|
+
timeoutMs?: number
|
|
69
|
+
/** Per-provider maximum input image bytes; larger images skip to a later provider or are compressed. */
|
|
70
|
+
maxImageBytes?: number
|
|
71
|
+
/** Per-provider maximum decoded pixel count per input image. */
|
|
72
|
+
maxImagePixels?: number
|
|
73
|
+
/** Per-provider in-flight request cap; an exhausted provider is skipped in favor of the next one. */
|
|
74
|
+
concurrency?: number
|
|
75
|
+
/** Total attempts against this provider before failing over to the next one (default 3). */
|
|
76
|
+
attempts?: number
|
|
77
|
+
}
|
|
78
|
+
|
|
47
79
|
/** Full user-facing configuration; every field defaults at the schema boundary. */
|
|
48
80
|
export interface VisionToolkitConfig {
|
|
49
81
|
provider?: {
|
|
@@ -60,6 +92,8 @@ export interface VisionToolkitConfig {
|
|
|
60
92
|
/** Outbound User-Agent for provider requests and connection tests. */
|
|
61
93
|
userAgent?: string
|
|
62
94
|
}
|
|
95
|
+
/** Ordered online vision providers; array order is the failover priority. */
|
|
96
|
+
providers?: VisionProviderConfig[]
|
|
63
97
|
/** Vision output language (`zh` or `en`). */
|
|
64
98
|
language?: 'zh' | 'en'
|
|
65
99
|
/** Single remote/upstream call budget in milliseconds. */
|
|
@@ -122,6 +156,22 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
|
|
|
122
156
|
anthropicThinking: z.union(['omit', 'disabled', 'adaptive'] as const).default('omit'),
|
|
123
157
|
userAgent: z.string().default(DEFAULT_VISION_USER_AGENT),
|
|
124
158
|
}),
|
|
159
|
+
providers: z.array(z.object({
|
|
160
|
+
name: z.string(),
|
|
161
|
+
enabled: z.boolean().default(true),
|
|
162
|
+
id: z.string(),
|
|
163
|
+
baseUrl: z.string(),
|
|
164
|
+
credential: z.string(),
|
|
165
|
+
model: z.string(),
|
|
166
|
+
protocol: z.union(['openai', 'anthropic'] as const).default('openai'),
|
|
167
|
+
anthropicThinking: z.union(['omit', 'disabled', 'adaptive'] as const).default('omit'),
|
|
168
|
+
userAgent: z.string(),
|
|
169
|
+
timeoutMs: z.number(),
|
|
170
|
+
maxImageBytes: z.number(),
|
|
171
|
+
maxImagePixels: z.number(),
|
|
172
|
+
concurrency: z.number(),
|
|
173
|
+
attempts: z.number(),
|
|
174
|
+
})).default([]),
|
|
125
175
|
language: z.union(['zh', 'en'] as const).default('zh'),
|
|
126
176
|
timeoutMs: z.number().default(30000),
|
|
127
177
|
maxImageBytes: z.number().default(4194304),
|
|
@@ -141,6 +191,25 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
|
|
|
141
191
|
}),
|
|
142
192
|
})
|
|
143
193
|
|
|
194
|
+
/** One resolved online vision provider, with every default materialized. */
|
|
195
|
+
export interface ResolvedProvider {
|
|
196
|
+
/** Stable unique id used to derive the auto-generated credential name. */
|
|
197
|
+
id?: string
|
|
198
|
+
name: string
|
|
199
|
+
enabled: boolean
|
|
200
|
+
baseUrl: string
|
|
201
|
+
credential: CredentialRef
|
|
202
|
+
model: string
|
|
203
|
+
protocol: 'openai' | 'anthropic'
|
|
204
|
+
anthropicThinking: 'omit' | 'disabled' | 'adaptive'
|
|
205
|
+
userAgent: string
|
|
206
|
+
timeoutMs: number
|
|
207
|
+
maxImageBytes: number
|
|
208
|
+
maxImagePixels: number
|
|
209
|
+
concurrency: number
|
|
210
|
+
attempts: number
|
|
211
|
+
}
|
|
212
|
+
|
|
144
213
|
/** Configuration after static validation, with every default materialized. */
|
|
145
214
|
export interface ResolvedVisionToolkitConfig {
|
|
146
215
|
provider: {
|
|
@@ -151,6 +220,8 @@ export interface ResolvedVisionToolkitConfig {
|
|
|
151
220
|
anthropicThinking: 'omit' | 'disabled' | 'adaptive'
|
|
152
221
|
userAgent: string
|
|
153
222
|
}
|
|
223
|
+
/** Ordered failover pool; array order is the priority, highest first. */
|
|
224
|
+
providers: ResolvedProvider[]
|
|
154
225
|
language: 'zh' | 'en'
|
|
155
226
|
timeoutMs: number
|
|
156
227
|
maxImageBytes: number
|
|
@@ -174,48 +245,141 @@ const MAX_TIMEOUT_MS = 600000
|
|
|
174
245
|
const MAX_IMAGE_BYTES = 268435456
|
|
175
246
|
const MAX_IMAGE_PIXELS = 268435456
|
|
176
247
|
const MAX_CONCURRENCY = 16
|
|
248
|
+
const MAX_PROVIDERS = 32
|
|
249
|
+
const MAX_PROVIDER_ATTEMPTS = 100
|
|
250
|
+
const DEFAULT_PROVIDER_ATTEMPTS = 3
|
|
251
|
+
|
|
252
|
+
/** Global limits a provider inherits when it does not set its own. */
|
|
253
|
+
interface ProviderDefaults {
|
|
254
|
+
timeoutMs: number
|
|
255
|
+
maxImageBytes: number
|
|
256
|
+
maxImagePixels: number
|
|
257
|
+
concurrency: number
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** How strictly one provider's connection fields are validated. */
|
|
261
|
+
type ProviderResolveMode = 'legacy' | 'enabled' | 'disabled'
|
|
177
262
|
|
|
178
263
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* @param config - parsed config with defaults applied.
|
|
184
|
-
* @returns the fully defaulted, validated configuration.
|
|
264
|
+
* Resolve and validate one provider (a legacy `provider` entry or an element
|
|
265
|
+
* of `providers`). `enabled` rejects absent or blank connection fields;
|
|
266
|
+
* `legacy` fills absent fields with the built-in free-vision defaults but
|
|
267
|
+
* still rejects explicit blank values; `disabled` is fully lenient.
|
|
185
268
|
*/
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
269
|
+
function resolveProvider(
|
|
270
|
+
input: VisionProviderConfig,
|
|
271
|
+
defaults: ProviderDefaults,
|
|
272
|
+
label: string,
|
|
273
|
+
mode: ProviderResolveMode,
|
|
274
|
+
): ResolvedProvider {
|
|
275
|
+
const baseUrlInput = input.baseUrl
|
|
276
|
+
const modelInput = input.model
|
|
277
|
+
const credentialInput = input.credential
|
|
278
|
+
const isBlank = (value: string | undefined): boolean => value === undefined || value.trim() === ''
|
|
279
|
+
|
|
280
|
+
let baseUrl: string
|
|
281
|
+
if (mode === 'enabled') {
|
|
282
|
+
if (isBlank(baseUrlInput)) throw new VisionToolkitError('config', `${label}.baseUrl must not be empty`)
|
|
283
|
+
baseUrl = baseUrlInput!.trim().replace(/\/+$/, '')
|
|
284
|
+
} else if (mode === 'legacy') {
|
|
285
|
+
baseUrl = baseUrlInput === undefined ? BUILT_IN_FREE_VISION_BASE_URL : baseUrlInput.trim().replace(/\/+$/, '')
|
|
286
|
+
} else {
|
|
287
|
+
baseUrl = isBlank(baseUrlInput) ? BUILT_IN_FREE_VISION_BASE_URL : baseUrlInput!.trim().replace(/\/+$/, '')
|
|
288
|
+
}
|
|
190
289
|
if (!/^https?:\/\//i.test(baseUrl) || baseUrl.length <= 'https://'.length) {
|
|
191
|
-
throw new VisionToolkitError('config',
|
|
290
|
+
throw new VisionToolkitError('config', `${label}.baseUrl must be an http(s) URL`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let model: string
|
|
294
|
+
if (mode === 'enabled') {
|
|
295
|
+
if (isBlank(modelInput)) throw new VisionToolkitError('config', `${label}.model must not be empty`)
|
|
296
|
+
model = modelInput!.trim()
|
|
297
|
+
} else if (mode === 'legacy') {
|
|
298
|
+
model = modelInput === undefined ? BUILT_IN_FREE_VISION_MODEL : modelInput.trim()
|
|
299
|
+
if (model.length === 0) {
|
|
300
|
+
throw new VisionToolkitError('config', `${label}.model must not be empty`)
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
model = isBlank(modelInput) ? BUILT_IN_FREE_VISION_MODEL : modelInput!.trim()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let credentialSource: string
|
|
307
|
+
if (mode === 'enabled') {
|
|
308
|
+
if (isBlank(credentialInput)) throw new VisionToolkitError('config', `${label}.credential must not be empty`)
|
|
309
|
+
credentialSource = credentialInput!.trim()
|
|
310
|
+
} else if (mode === 'legacy') {
|
|
311
|
+
credentialSource = credentialInput === undefined ? BUILT_IN_FREE_VISION_CREDENTIAL : credentialInput.trim()
|
|
312
|
+
} else {
|
|
313
|
+
credentialSource = isBlank(credentialInput) ? BUILT_IN_FREE_VISION_CREDENTIAL : credentialInput!.trim()
|
|
192
314
|
}
|
|
193
315
|
let credential: CredentialRef
|
|
194
316
|
try {
|
|
195
|
-
credential = credentialRef(
|
|
317
|
+
credential = credentialRef(credentialSource)
|
|
196
318
|
} catch (error) {
|
|
197
|
-
throw new VisionToolkitError(
|
|
198
|
-
'config',
|
|
199
|
-
`provider.credential "${provider.credential ?? BUILT_IN_FREE_VISION_CREDENTIAL}" is not a valid credential reference`,
|
|
200
|
-
{ cause: error },
|
|
201
|
-
)
|
|
319
|
+
throw new VisionToolkitError('config', `${label}.credential "${credentialSource}" is not a valid credential reference`, { cause: error })
|
|
202
320
|
}
|
|
203
|
-
const
|
|
204
|
-
if (model.length === 0) {
|
|
205
|
-
throw new VisionToolkitError('config', 'provider.model must not be empty')
|
|
206
|
-
}
|
|
207
|
-
const protocol = provider.protocol ?? 'openai'
|
|
321
|
+
const protocol = input.protocol ?? 'openai'
|
|
208
322
|
if (protocol !== 'openai' && protocol !== 'anthropic') {
|
|
209
|
-
throw new VisionToolkitError('config',
|
|
323
|
+
throw new VisionToolkitError('config', `${label}.protocol must be "openai" or "anthropic"`)
|
|
210
324
|
}
|
|
211
|
-
const anthropicThinking =
|
|
325
|
+
const anthropicThinking = input.anthropicThinking ?? 'omit'
|
|
212
326
|
if (anthropicThinking !== 'omit' && anthropicThinking !== 'disabled' && anthropicThinking !== 'adaptive') {
|
|
213
|
-
throw new VisionToolkitError('config',
|
|
327
|
+
throw new VisionToolkitError('config', `${label}.anthropicThinking must be "omit", "disabled", or "adaptive"`)
|
|
214
328
|
}
|
|
215
|
-
const userAgent = (
|
|
329
|
+
const userAgent = (input.userAgent ?? DEFAULT_VISION_USER_AGENT).trim()
|
|
216
330
|
if (userAgent.length === 0) {
|
|
217
|
-
throw new VisionToolkitError('config',
|
|
331
|
+
throw new VisionToolkitError('config', `${label}.userAgent must not be empty`)
|
|
218
332
|
}
|
|
333
|
+
const timeoutMs = input.timeoutMs ?? defaults.timeoutMs
|
|
334
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > MAX_TIMEOUT_MS) {
|
|
335
|
+
throw new VisionToolkitError('config', `${label}.timeoutMs must be an integer between 1000 and ${MAX_TIMEOUT_MS}`)
|
|
336
|
+
}
|
|
337
|
+
const maxImageBytes = input.maxImageBytes ?? defaults.maxImageBytes
|
|
338
|
+
if (!Number.isInteger(maxImageBytes) || maxImageBytes < 1024 || maxImageBytes > MAX_IMAGE_BYTES) {
|
|
339
|
+
throw new VisionToolkitError('config', `${label}.maxImageBytes must be an integer between 1024 and ${MAX_IMAGE_BYTES}`)
|
|
340
|
+
}
|
|
341
|
+
const maxImagePixels = input.maxImagePixels ?? defaults.maxImagePixels
|
|
342
|
+
if (!Number.isInteger(maxImagePixels) || maxImagePixels < 1 || maxImagePixels > MAX_IMAGE_PIXELS) {
|
|
343
|
+
throw new VisionToolkitError('config', `${label}.maxImagePixels must be an integer between 1 and ${MAX_IMAGE_PIXELS}`)
|
|
344
|
+
}
|
|
345
|
+
const concurrency = input.concurrency ?? defaults.concurrency
|
|
346
|
+
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > MAX_CONCURRENCY) {
|
|
347
|
+
throw new VisionToolkitError('config', `${label}.concurrency must be an integer between 1 and ${MAX_CONCURRENCY}`)
|
|
348
|
+
}
|
|
349
|
+
const attempts = input.attempts ?? DEFAULT_PROVIDER_ATTEMPTS
|
|
350
|
+
if (!Number.isInteger(attempts) || attempts < 1 || attempts > MAX_PROVIDER_ATTEMPTS) {
|
|
351
|
+
throw new VisionToolkitError('config', `${label}.attempts must be an integer between 1 and ${MAX_PROVIDER_ATTEMPTS}`)
|
|
352
|
+
}
|
|
353
|
+
const name = (input.name ?? '').trim()
|
|
354
|
+
const id = (input.id ?? '').trim()
|
|
355
|
+
return {
|
|
356
|
+
...(id.length === 0 ? {} : { id }),
|
|
357
|
+
name: name.length === 0 ? model : name,
|
|
358
|
+
enabled: input.enabled !== false,
|
|
359
|
+
baseUrl,
|
|
360
|
+
credential,
|
|
361
|
+
model,
|
|
362
|
+
protocol,
|
|
363
|
+
anthropicThinking,
|
|
364
|
+
userAgent,
|
|
365
|
+
timeoutMs,
|
|
366
|
+
maxImageBytes,
|
|
367
|
+
maxImagePixels,
|
|
368
|
+
concurrency,
|
|
369
|
+
attempts,
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Validate and normalize a config object (partial inputs receive the same
|
|
375
|
+
* defaults the schemastery schema applies). Configuration mistakes fail loud
|
|
376
|
+
* at plugin load (the earliest resolvable point); runtime availability is a
|
|
377
|
+
* separate, later concern.
|
|
378
|
+
* @param config - parsed config with defaults applied.
|
|
379
|
+
* @returns the fully defaulted, validated configuration.
|
|
380
|
+
*/
|
|
381
|
+
export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionToolkitConfig {
|
|
382
|
+
const runtime = config.runtime ?? {}
|
|
219
383
|
const language = config.language ?? 'zh'
|
|
220
384
|
if (language !== 'zh' && language !== 'en') {
|
|
221
385
|
throw new VisionToolkitError('config', 'language must be "zh" or "en"')
|
|
@@ -259,8 +423,26 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
|
|
|
259
423
|
const variantProviders = (imageInputVariants.providers ?? [])
|
|
260
424
|
.map(provider => provider.trim())
|
|
261
425
|
.filter(provider => provider.length > 0)
|
|
426
|
+
const providerDefaults: ProviderDefaults = { timeoutMs, maxImageBytes, maxImagePixels, concurrency }
|
|
427
|
+
const configuredProviders = config.providers ?? []
|
|
428
|
+
if (configuredProviders.length > MAX_PROVIDERS) {
|
|
429
|
+
throw new VisionToolkitError('config', `providers must have at most ${MAX_PROVIDERS} entries`)
|
|
430
|
+
}
|
|
431
|
+
const providers: ResolvedProvider[] = configuredProviders.length > 0
|
|
432
|
+
? configuredProviders.map((entry, index) =>
|
|
433
|
+
resolveProvider(entry, providerDefaults, `providers[${index}]`, entry.enabled !== false ? 'enabled' : 'disabled'))
|
|
434
|
+
: [resolveProvider(config.provider ?? {}, providerDefaults, 'provider', 'legacy')]
|
|
435
|
+
const primary = providers.find(entry => entry.enabled) ?? providers[0]!
|
|
262
436
|
return {
|
|
263
|
-
provider: {
|
|
437
|
+
provider: {
|
|
438
|
+
baseUrl: primary.baseUrl,
|
|
439
|
+
credential: primary.credential,
|
|
440
|
+
model: primary.model,
|
|
441
|
+
protocol: primary.protocol,
|
|
442
|
+
anthropicThinking: primary.anthropicThinking,
|
|
443
|
+
userAgent: primary.userAgent,
|
|
444
|
+
},
|
|
445
|
+
providers,
|
|
264
446
|
language,
|
|
265
447
|
timeoutMs,
|
|
266
448
|
maxImageBytes,
|
package/src/evidence-cache.ts
CHANGED
|
@@ -93,6 +93,20 @@ export function evidenceRuntimeFingerprint(
|
|
|
93
93
|
sslVerify: sslVerify ?? null,
|
|
94
94
|
userAgent: config.provider.userAgent,
|
|
95
95
|
},
|
|
96
|
+
providers: config.providers.map(provider => ({
|
|
97
|
+
name: provider.name,
|
|
98
|
+
enabled: provider.enabled,
|
|
99
|
+
baseUrl: provider.baseUrl,
|
|
100
|
+
credential: String(provider.credential),
|
|
101
|
+
model: provider.model,
|
|
102
|
+
protocol: provider.protocol,
|
|
103
|
+
anthropicThinking: provider.anthropicThinking,
|
|
104
|
+
userAgent: provider.userAgent,
|
|
105
|
+
maxImageBytes: provider.maxImageBytes,
|
|
106
|
+
maxImagePixels: provider.maxImagePixels,
|
|
107
|
+
concurrency: provider.concurrency,
|
|
108
|
+
attempts: provider.attempts,
|
|
109
|
+
})),
|
|
96
110
|
language: config.language,
|
|
97
111
|
timeoutMs: config.timeoutMs,
|
|
98
112
|
concurrency: config.concurrency,
|
|
@@ -95,6 +95,18 @@ export function variantProviderId(upstream: string): string {
|
|
|
95
95
|
return `${VARIANT_PROVIDER_PREFIX}${upstream}`
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** Stable semantic key for detecting a provider retry-policy replacement. */
|
|
99
|
+
function retryPolicyKey(policy: ResolvedRetryPolicy): string {
|
|
100
|
+
const backoff = [policy.initialDelayMs, policy.maxDelayMs, policy.jitterRatio]
|
|
101
|
+
if (policy.mode === 'always') return JSON.stringify([policy.mode, ...backoff])
|
|
102
|
+
return JSON.stringify([
|
|
103
|
+
policy.mode,
|
|
104
|
+
policy.maxRetries,
|
|
105
|
+
[...policy.retryableCodes].sort(),
|
|
106
|
+
...backoff,
|
|
107
|
+
])
|
|
108
|
+
}
|
|
109
|
+
|
|
98
110
|
/**
|
|
99
111
|
* Whether one model earns an image-input variant: the host must positively
|
|
100
112
|
* declare it text-only. A model with unknown modalities is left alone — its
|
|
@@ -516,6 +528,10 @@ export class ImageInputVariantAdapter extends LlmAdapter {
|
|
|
516
528
|
}
|
|
517
529
|
}
|
|
518
530
|
|
|
531
|
+
override providerRetryPolicy(_provider: string): ResolvedRetryPolicy {
|
|
532
|
+
return this.llm.providerRetryPolicy(this.upstream)
|
|
533
|
+
}
|
|
534
|
+
|
|
519
535
|
override async listModels(provider: string): Promise<readonly LlmModelInfo[]> {
|
|
520
536
|
const models = await this.llm.listModels(this.upstream)
|
|
521
537
|
return models.filter(shouldWrapModel).map((model) => ({
|
|
@@ -552,15 +568,22 @@ export class ImageInputVariantAdapter extends LlmAdapter {
|
|
|
552
568
|
}
|
|
553
569
|
|
|
554
570
|
/**
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
571
|
+
* Prepare one model call through this variant generation. Newer host
|
|
572
|
+
* `LlmAdapter` contracts call this before dispatch; mirror the base
|
|
573
|
+
* implementation so the variant route works with any installed dsh-llm
|
|
574
|
+
* version (older bases do not declare the method, hence no `override`).
|
|
558
575
|
*/
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
576
|
+
async prepareCall(
|
|
577
|
+
provider: string,
|
|
578
|
+
model: string,
|
|
579
|
+
signal?: AbortSignal,
|
|
580
|
+
): Promise<{
|
|
581
|
+
readonly model: LlmResolvedModelInfo
|
|
582
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
|
|
583
|
+
}> {
|
|
584
|
+
return {
|
|
585
|
+
model: await this.resolveModel(provider, model, signal),
|
|
586
|
+
stream: options => this.stream(options),
|
|
564
587
|
}
|
|
565
588
|
}
|
|
566
589
|
|
|
@@ -826,10 +849,9 @@ export function installImageInputVariants(
|
|
|
826
849
|
): { dispose: () => void; reconcile: () => void } {
|
|
827
850
|
const evidenceStore = new SessionEvidenceStore(ctx)
|
|
828
851
|
const evidenceCache = new EvidenceCache(EVIDENCE_CACHE_LIMIT, evidenceStore)
|
|
829
|
-
const registrations = new Map<string, () => void>()
|
|
830
|
-
// The host snapshots adapter provider metadata
|
|
831
|
-
//
|
|
832
|
-
// every wrapper for the new names to reach the model selector.
|
|
852
|
+
const registrations = new Map<string, { dispose: () => void; retryPolicyKey: string }>()
|
|
853
|
+
// The host snapshots adapter provider metadata and retry policy at registration
|
|
854
|
+
// time, so changes to either require rebuilding the wrapper route.
|
|
833
855
|
let lastHidden = false
|
|
834
856
|
// Upstream ids observed missing and the timestamp of the first missing
|
|
835
857
|
// observation. A wrapper is only released after its upstream stays absent
|
|
@@ -846,12 +868,12 @@ export function installImageInputVariants(
|
|
|
846
868
|
let sweepQueued = false
|
|
847
869
|
|
|
848
870
|
const release = (upstream: string): void => {
|
|
849
|
-
const
|
|
850
|
-
if (
|
|
871
|
+
const registration = registrations.get(upstream)
|
|
872
|
+
if (registration === undefined) return
|
|
851
873
|
registrations.delete(upstream)
|
|
852
874
|
stale.delete(upstream)
|
|
853
875
|
try {
|
|
854
|
-
dispose()
|
|
876
|
+
registration.dispose()
|
|
855
877
|
} catch (error) {
|
|
856
878
|
ctx.logger.warn(
|
|
857
879
|
'dsh-vision-toolkit: image-input variant release failed for "%s": %s',
|
|
@@ -930,18 +952,28 @@ export function installImageInputVariants(
|
|
|
930
952
|
continue
|
|
931
953
|
}
|
|
932
954
|
const eligible = models.some(shouldWrapModel)
|
|
933
|
-
|
|
934
|
-
|
|
955
|
+
let upstreamRetryPolicyKey: string
|
|
956
|
+
try {
|
|
957
|
+
upstreamRetryPolicyKey = retryPolicyKey(llm.providerRetryPolicy(upstream))
|
|
958
|
+
} catch {
|
|
959
|
+
// Preserve an existing wrapper through a transient upstream registry
|
|
960
|
+
// gap; the next topology event or periodic sweep will retry the probe.
|
|
961
|
+
continue
|
|
962
|
+
}
|
|
963
|
+
let registration = registrations.get(upstream)
|
|
964
|
+
if (registration !== undefined && live.has(variantId)) {
|
|
935
965
|
// Re-read the live registry after the await: a rebuild inside the
|
|
936
966
|
// probe window can drop our wrapper without touching our map.
|
|
937
967
|
const liveNow = new Set(llm.listProviders().map(provider => provider.id))
|
|
938
968
|
if (liveNow.has(variantId)) {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
969
|
+
if (eligible && registration.retryPolicyKey === upstreamRetryPolicyKey) continue
|
|
970
|
+
// Eligibility and retry policy are registration-time facts. Rebuild
|
|
971
|
+
// whenever either no longer matches the live upstream route.
|
|
972
|
+
release(upstream)
|
|
973
|
+
registration = undefined
|
|
942
974
|
}
|
|
943
975
|
}
|
|
944
|
-
if (
|
|
976
|
+
if (registration !== undefined) {
|
|
945
977
|
// Dead handle from a registry rebuild/reset: drop it and register a
|
|
946
978
|
// fresh wrapper below.
|
|
947
979
|
release(upstream)
|
|
@@ -961,7 +993,7 @@ export function installImageInputVariants(
|
|
|
961
993
|
() => getConfig().imageInputVariants.hidden,
|
|
962
994
|
),
|
|
963
995
|
)
|
|
964
|
-
registrations.set(upstream, dispose)
|
|
996
|
+
registrations.set(upstream, { dispose, retryPolicyKey: upstreamRetryPolicyKey })
|
|
965
997
|
stale.delete(upstream)
|
|
966
998
|
} catch (error) {
|
|
967
999
|
ctx.logger.warn(
|