dsh-vision-router 2.3.0 → 3.0.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.md +18 -3
- package/README.zh.md +16 -3
- package/docs/architecture/3.0-architecture-plan.md +252 -0
- package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
- package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
- package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
- package/docs/architecture/3.0-r11-adversarial-review.md +447 -0
- package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
- package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
- package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
- package/docs/architecture/3.0-r7-public-api.md +151 -0
- package/docs/architecture/3.0-r8-ci-release.md +248 -0
- package/docs/architecture/3.0-r9-final-audit.md +370 -0
- package/docs/architecture/dsh-support-window.md +13 -3
- package/docs/releases/v3.0.0.md +85 -0
- package/docs/releases/v3.0.1.md +77 -0
- package/entry.js +1 -1
- package/index.js +110 -71
- package/lib/agent-request-route-authority.js +1 -20
- package/lib/core-apply-lifecycle.js +37 -43
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +11 -144
- package/lib/core-vision-surface-domain.js +63 -0
- package/lib/core-vision-surface.js +11 -77
- package/lib/delegated-call-config.js +15 -24
- package/lib/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +26 -230
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- package/lib/file-logger.js +8 -3
- package/lib/grounding-coordinate-runtime.js +1 -2
- package/lib/http-body-limit.js +97 -79
- package/lib/image-offload-compat.js +30 -21
- package/lib/image-resource-governor.js +210 -228
- package/lib/legacy-global-proxy-boundary.js +46 -3
- package/lib/live-model-client-prelude.js +12 -4
- package/lib/live-model-discovery.js +134 -43
- package/lib/local-vision-stabilizer.js +33 -18
- package/lib/maintenance-routes.js +1 -3
- package/lib/native-image-coexistence.js +15 -3
- package/lib/ollama-cold-start.js +87 -23
- package/lib/pi-ai-bridge-wire-compat.js +3 -2
- package/lib/proxy-dispatcher-pool.js +151 -92
- package/lib/proxy-routing.js +75 -51
- package/lib/proxy-url-compat.js +3 -10
- package/lib/public-config.js +23 -44
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +17 -4
- package/lib/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +30 -10
- package/lib/screenshot-source-boundary.js +2 -2
- package/lib/session-affinity-runtime.js +113 -97
- package/lib/session-affinity.js +89 -76
- package/lib/session-event-history-compat.js +30 -0
- package/lib/session-query-readers.js +263 -0
- package/lib/session-surface-compat.js +30 -31
- package/lib/session-surface-policy-domain.js +85 -0
- package/lib/session-surface-policy.js +2 -111
- package/lib/session-turn-resolver.js +2 -6
- package/lib/session-vision-attachment-index.js +143 -0
- package/lib/session-vision-delegation-boundary.js +34 -0
- package/lib/session-vision-delegation.js +22 -0
- package/lib/session-vision-index.js +25 -107
- package/lib/session-vision-mode-authority.js +229 -192
- package/lib/session-vision-mode-boundary.js +65 -22
- package/lib/session-vision-policy-domain.js +304 -0
- package/lib/session-vision-policy.js +104 -0
- package/lib/session-vision-state.js +368 -378
- package/lib/settings-client-017-compat.js +16 -10
- package/lib/structured-flow-hardening.js +22 -4
- package/lib/tesseract-exec-compat.js +9 -1
- package/lib/turn-budget-context.js +47 -41
- package/lib/update-check.js +11 -2
- package/lib/vision-attachment-handle-runtime.js +176 -156
- package/lib/vision-backend-runtime-policy.js +16 -0
- package/lib/vision-background-benchmark.js +55 -6
- package/lib/vision-background-failure-policy.js +41 -41
- package/lib/vision-background-stop-store-core.js +217 -0
- package/lib/vision-background-stop-store.js +8 -179
- package/lib/vision-capability-benchmark.js +11 -61
- package/lib/vision-capability-evidence.js +35 -0
- package/lib/vision-capability-fingerprint.js +72 -0
- package/lib/vision-capability-identity.js +116 -61
- package/lib/vision-capability-router.js +13 -33
- package/lib/vision-chunk-assembler.js +95 -0
- package/lib/vision-diagnostics-routes.js +42 -4
- package/lib/vision-execution-order-apply.js +45 -55
- package/lib/vision-execution-order.js +19 -19
- package/lib/vision-execution-policy.js +12 -7
- package/lib/vision-model-registry.js +30 -17
- package/lib/vision-model-visibility-boundary-main.js +15 -16
- package/lib/vision-provider-transport.js +137 -147
- package/lib/vision-routing-authority.js +47 -48
- package/lib/vision-routing-product.js +26 -29
- package/lib/vision-routing-settings-prelude.js +8 -3
- package/lib/vision-runtime-performance-store.js +166 -0
- package/lib/vision-runtime-performance.js +13 -142
- package/lib/vision-tool-runtime-boundary.js +99 -30
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +4 -7
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- package/package.json +37 -11
package/index.js
CHANGED
|
@@ -1049,8 +1049,7 @@ export function apply(ctx, config = {}, runtime = {}) {
|
|
|
1049
1049
|
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
1050
1050
|
},
|
|
1051
1051
|
}
|
|
1052
|
-
|
|
1053
|
-
ctx.effect(() => httpHandle, 'vision-router: vision-http route')
|
|
1052
|
+
ctx.llm.registerAdapter([HTTP_ROUTE], httpAdapter)
|
|
1054
1053
|
|
|
1055
1054
|
}
|
|
1056
1055
|
|
|
@@ -1354,7 +1353,7 @@ export function apply(ctx, config = {}, runtime = {}) {
|
|
|
1354
1353
|
const spec = wanted.get(provider)
|
|
1355
1354
|
if (spec === undefined) {
|
|
1356
1355
|
try {
|
|
1357
|
-
held.
|
|
1356
|
+
held.registration()
|
|
1358
1357
|
twinHandles.delete(provider)
|
|
1359
1358
|
} catch (error) {
|
|
1360
1359
|
ctx.logger?.warn(
|
|
@@ -1372,7 +1371,7 @@ export function apply(ctx, config = {}, runtime = {}) {
|
|
|
1372
1371
|
held.state.models = spec.models
|
|
1373
1372
|
held.state.sourceName = spec.sourceName
|
|
1374
1373
|
try {
|
|
1375
|
-
held.
|
|
1374
|
+
held.registration.replace([`${provider}-vision`])
|
|
1376
1375
|
held.key = nextKey
|
|
1377
1376
|
} catch (error) {
|
|
1378
1377
|
held.state.models = previousModels
|
|
@@ -1394,10 +1393,12 @@ export function apply(ctx, config = {}, runtime = {}) {
|
|
|
1394
1393
|
const twinRoute = `${provider}-vision`
|
|
1395
1394
|
const state = { models: spec.models, sourceName: spec.sourceName }
|
|
1396
1395
|
try {
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1396
|
+
const registration = ctx.llm.registerAdapter(
|
|
1397
|
+
[twinRoute],
|
|
1398
|
+
makeTwinAdapter(provider, state),
|
|
1399
|
+
)
|
|
1399
1400
|
twinHandles.set(provider, {
|
|
1400
|
-
|
|
1401
|
+
registration,
|
|
1401
1402
|
state,
|
|
1402
1403
|
key: twinSpecKey(spec.models, spec.sourceName),
|
|
1403
1404
|
})
|
|
@@ -4666,34 +4667,48 @@ ctx.logger?.info(
|
|
|
4666
4667
|
activateDeepTools = () => {
|
|
4667
4668
|
if (deepActive) return '视觉深看工具已在挂载状态。'
|
|
4668
4669
|
deepActive = true
|
|
4669
|
-
|
|
4670
|
-
const
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4670
|
+
try {
|
|
4671
|
+
for (const def of deepToolDefs) {
|
|
4672
|
+
const registeredDef =
|
|
4673
|
+
def.name === 'vision_bootstrap' || typeof def.execute !== 'function'
|
|
4674
|
+
? def
|
|
4675
|
+
: {
|
|
4676
|
+
...def,
|
|
4677
|
+
async execute(args, exec) {
|
|
4678
|
+
const session = exec && exec.agent && exec.agent.session
|
|
4679
|
+
const state = session ? structuredBootstrapTurnState.get(session) : undefined
|
|
4680
|
+
if (structuredBootstrapEnabled() && state && state.required && state.completed !== true) {
|
|
4681
|
+
return JSON.stringify({
|
|
4682
|
+
ok: false,
|
|
4683
|
+
code: state.failed ? 'STRUCTURED_BOOTSTRAP_FAILED' : 'STRUCTURED_BOOTSTRAP_REQUIRED',
|
|
4684
|
+
retryable: !state.failed,
|
|
4685
|
+
reason: state.failed
|
|
4686
|
+
? 'the required structured bootstrap visual pass failed; do not make more visual calls this turn'
|
|
4687
|
+
: 'call vision_bootstrap and wait for its universal structured visual result before any other vision tool',
|
|
4688
|
+
})
|
|
4689
|
+
}
|
|
4690
|
+
// 识图档位不在这里做调用次数拦截;显式 visionDepthMaxCalls 由
|
|
4691
|
+
// structured-flow hardening 统一执行,避免与 evidence 完成状态重复计数。
|
|
4692
|
+
// Tool-specific execution policy belongs to the tool itself; this wrapper owns
|
|
4693
|
+
// only bootstrap ordering and never rewrites model/user arguments.
|
|
4694
|
+
const result = await def.execute(args, exec)
|
|
4695
|
+
return result
|
|
4696
|
+
},
|
|
4697
|
+
}
|
|
4698
|
+
deepDisposers.push(ctx.tools.register(registeredDef))
|
|
4699
|
+
}
|
|
4700
|
+
} catch (error) {
|
|
4701
|
+
// A partial mount must never be reported as mounted: retire what this
|
|
4702
|
+
// attempt registered, release the latch and surface the failure.
|
|
4703
|
+
deepDisposers.splice(0).forEach((dispose) => {
|
|
4704
|
+
try {
|
|
4705
|
+
dispose()
|
|
4706
|
+
} catch {
|
|
4707
|
+
// Rollback is best effort; the original registration error wins.
|
|
4708
|
+
}
|
|
4709
|
+
})
|
|
4710
|
+
deepActive = false
|
|
4711
|
+
throw error
|
|
4697
4712
|
}
|
|
4698
4713
|
return (
|
|
4699
4714
|
'视觉深看工具已挂载:vision_bootstrap(结构化预识别)、vision_describe(看图问答)、vision_ground(像素定位)、vision_detect(元素清单)、' +
|
|
@@ -4703,20 +4718,32 @@ ctx.logger?.info(
|
|
|
4703
4718
|
'注意:vision_ocr 只用于读取图片文字;视觉工具返回 ok:false 后端不可用结果时,不要改问法重复调用,继续文本任务。'
|
|
4704
4719
|
)
|
|
4705
4720
|
}
|
|
4721
|
+
ctx.effect(
|
|
4722
|
+
() => () => {
|
|
4723
|
+
deepDisposers.splice(0).forEach((dispose) => dispose())
|
|
4724
|
+
deepActive = false
|
|
4725
|
+
},
|
|
4726
|
+
'vision-router: deep tools',
|
|
4727
|
+
)
|
|
4728
|
+
|
|
4706
4729
|
if (progressive) {
|
|
4707
|
-
ctx.
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4730
|
+
ctx.effect(
|
|
4731
|
+
() =>
|
|
4732
|
+
ctx.tools.register({
|
|
4733
|
+
name: 'vision_activate',
|
|
4734
|
+
description:
|
|
4735
|
+
'Mount the deep vision tools (vision_bootstrap / vision_describe / vision_ground / vision_detect / vision_materialize / vision_crop / ' +
|
|
4736
|
+
'vision_pixel_diff / vision_colors / vision_ocr / vision_trace / ' +
|
|
4737
|
+
'vision_extract_foreground / vision_present / vision_html_screenshot) for this session. Desktop screenshot remains disabled until the user explicitly opts in through Vision Router settings. They mount ' +
|
|
4738
|
+
'automatically on image turns; call this only when you need them on a text-only turn.',
|
|
4739
|
+
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
|
4740
|
+
output: stringOutput,
|
|
4741
|
+
async execute() {
|
|
4742
|
+
return activateDeepTools()
|
|
4743
|
+
},
|
|
4744
|
+
}),
|
|
4745
|
+
'vision-router: progressive bootstrap tool',
|
|
4746
|
+
)
|
|
4720
4747
|
const skills = ctx.get('skills')
|
|
4721
4748
|
if (skills !== undefined && typeof skills.register === 'function') {
|
|
4722
4749
|
ctx.effect(
|
|
@@ -4767,13 +4794,6 @@ ctx.logger?.info(
|
|
|
4767
4794
|
} else {
|
|
4768
4795
|
activateDeepTools()
|
|
4769
4796
|
}
|
|
4770
|
-
ctx.effect(
|
|
4771
|
-
() => () => {
|
|
4772
|
-
deepDisposers.splice(0).forEach((dispose) => dispose())
|
|
4773
|
-
deepActive = false
|
|
4774
|
-
},
|
|
4775
|
-
'vision-router: deep tools',
|
|
4776
|
-
)
|
|
4777
4797
|
}
|
|
4778
4798
|
|
|
4779
4799
|
// ── settings seam: the Web 设置 > 插件 > 插件配置 panel owns a
|
|
@@ -4784,6 +4804,9 @@ ctx.logger?.info(
|
|
|
4784
4804
|
// @deepseek-ai/dsh-settings: the published npm build trails the deployment,
|
|
4785
4805
|
// and the service API is the stable contract here.
|
|
4786
4806
|
ctx.inject(['settings'], (sctx) => {
|
|
4807
|
+
// Registration and the live-settings subscription are Host contract calls:
|
|
4808
|
+
// they must happen for every supported Host, independently of whether the
|
|
4809
|
+
// lifecycle seam below can run eagerly. Only ownership needs the factory.
|
|
4787
4810
|
const scope = sctx.settings.register('vision-router', Config, {
|
|
4788
4811
|
base: config,
|
|
4789
4812
|
})
|
|
@@ -4791,26 +4814,43 @@ ctx.logger?.info(
|
|
|
4791
4814
|
// With the settings document now visible, reconcile the routing mounts
|
|
4792
4815
|
// (wrapper route, chain route) against the resolved values.
|
|
4793
4816
|
syncRoutingMounts()
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4817
|
+
let disposeWatch
|
|
4818
|
+
if (typeof scope.watch === 'function') {
|
|
4819
|
+
disposeWatch = scope.watch(() => {
|
|
4820
|
+
// Most consumers read current() per call, but the wrappedProviders
|
|
4821
|
+
// twins and the routing mounts are registered eagerly: re-sync them
|
|
4822
|
+
// whenever the settings document loads or the user edits the card.
|
|
4823
|
+
syncTwins()
|
|
4824
|
+
syncRoutingMounts()
|
|
4825
|
+
})
|
|
4826
|
+
}
|
|
4827
|
+
const restore = () => {
|
|
4828
|
+
if (typeof disposeWatch === 'function') disposeWatch()
|
|
4829
|
+
disposeWatch = undefined
|
|
4830
|
+
// The settings provider went away: fall back to the composition entry.
|
|
4831
|
+
current = () => config
|
|
4832
|
+
}
|
|
4833
|
+
if (typeof sctx.effect !== 'function') {
|
|
4834
|
+
// No lifecycle seam: this injection's context owns the source, so there is
|
|
4835
|
+
// nothing for DVR to restore when the Settings service itself goes away.
|
|
4836
|
+
return
|
|
4837
|
+
}
|
|
4838
|
+
try {
|
|
4839
|
+
sctx.effect(() => restore, 'vision-router: settings source')
|
|
4840
|
+
} catch (error) {
|
|
4841
|
+
// Registration already published the scope and its subscription: withdraw
|
|
4842
|
+
// both before the failure escapes, exactly as the owner would on unload.
|
|
4843
|
+
restore()
|
|
4844
|
+
throw error
|
|
4845
|
+
}
|
|
4808
4846
|
})
|
|
4809
4847
|
|
|
4810
4848
|
// Product diagnostics/settings support is a separate Web owner. Core supplies
|
|
4811
4849
|
// only the three coherent domain faces it already computes; route lifecycle,
|
|
4812
4850
|
// bounded /models probing and JSON response semantics live outside Core.
|
|
4813
4851
|
installVisionDiagnosticsRoutes(ctx, {
|
|
4852
|
+
// Same egress as real vision turns: the Router-owned provider transport.
|
|
4853
|
+
transport: runtime?.providerTransport,
|
|
4814
4854
|
connection: {
|
|
4815
4855
|
candidatePairs: pairs,
|
|
4816
4856
|
localBackends: () => localProvidersOf(current()),
|
|
@@ -4836,7 +4876,7 @@ ctx.logger?.info(
|
|
|
4836
4876
|
// (plus a fixed product allowlist) — without this directory entry the Web
|
|
4837
4877
|
// card's settingsScope binder reports the namespace as unavailable.
|
|
4838
4878
|
try {
|
|
4839
|
-
|
|
4879
|
+
ctx.llm.registerConfigurableProviders([
|
|
4840
4880
|
{
|
|
4841
4881
|
provider: 'vision-router',
|
|
4842
4882
|
displayName: '视觉路由(自动识图)',
|
|
@@ -4844,7 +4884,6 @@ ctx.logger?.info(
|
|
|
4844
4884
|
settingsPath: [],
|
|
4845
4885
|
},
|
|
4846
4886
|
])
|
|
4847
|
-
ctx.effect(() => providerDirectory, 'vision-router: configurable provider directory')
|
|
4848
4887
|
} catch (error) {
|
|
4849
4888
|
ctx.logger?.warn(
|
|
4850
4889
|
'vision-router: configurable provider registration failed: %s',
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { projectDelegatedCallConfig } from './delegated-call-config.js'
|
|
2
2
|
import { contextWithTwinImageCapabilityFallback } from './twin-image-capability-fallback.js'
|
|
3
3
|
|
|
4
|
-
const routeAuthorityOptionsByContext = new WeakMap()
|
|
5
|
-
|
|
6
4
|
function isObject(value) {
|
|
7
5
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
8
6
|
}
|
|
@@ -30,21 +28,6 @@ export function projectAgentRequestRouteHandoff(sourceConfig, resultConfig) {
|
|
|
30
28
|
return projectDelegatedCallConfig(resultConfig)
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
/**
|
|
34
|
-
* Runtime composition can publish route identities before Core.apply without
|
|
35
|
-
* changing the mature `contextWithAgentRequestRouteAuthority(backendRuntimeCtx)`
|
|
36
|
-
* boundary shape. The WeakMap keeps this per-context and garbage-collectable.
|
|
37
|
-
*/
|
|
38
|
-
export function configureAgentRequestRouteAuthority(ctx, options = {}) {
|
|
39
|
-
if (!isObject(ctx)) return ctx
|
|
40
|
-
routeAuthorityOptionsByContext.set(ctx, {
|
|
41
|
-
wrapperRoute: options.wrapperRoute,
|
|
42
|
-
chainRoute: options.chainRoute,
|
|
43
|
-
logger: options.logger,
|
|
44
|
-
})
|
|
45
|
-
return ctx
|
|
46
|
-
}
|
|
47
|
-
|
|
48
31
|
/**
|
|
49
32
|
* Private Core-facing context. It wraps only `agent/request` handlers and only
|
|
50
33
|
* projects a result when that handler demonstrably changed provider/model.
|
|
@@ -60,9 +43,7 @@ export function configureAgentRequestRouteAuthority(ctx, options = {}) {
|
|
|
60
43
|
*/
|
|
61
44
|
export function contextWithAgentRequestRouteAuthority(ctx, options) {
|
|
62
45
|
if (!isObject(ctx)) return ctx
|
|
63
|
-
const fallbackOptions = isObject(options)
|
|
64
|
-
? options
|
|
65
|
-
: routeAuthorityOptionsByContext.get(ctx) ?? {}
|
|
46
|
+
const fallbackOptions = isObject(options) ? options : {}
|
|
66
47
|
const routeAuthorityCtx = new Proxy(ctx, {
|
|
67
48
|
get(target, property) {
|
|
68
49
|
if (property !== 'on') {
|
|
@@ -1,47 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Preserve Core's synchronous apply contract while giving Promise-like Core
|
|
3
|
-
* implementations one explicit settlement boundary for post-registration work.
|
|
4
|
-
* This helper owns no runtime state; callers provide success/failure lifecycle
|
|
5
|
-
* effects explicitly.
|
|
6
|
-
*/
|
|
7
1
|
export function runCoreApplyLifecycle(invoke, { onSuccess, onFailure } = {}) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
onFailure?.(error)
|
|
13
|
-
} catch {
|
|
14
|
-
// Cleanup/diagnostic failure must not replace the Core apply error.
|
|
15
|
-
}
|
|
16
|
-
throw error
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let result
|
|
20
|
-
try {
|
|
21
|
-
result = invoke()
|
|
22
|
-
} catch (error) {
|
|
23
|
-
return fail(error)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (result && typeof result.then === 'function') {
|
|
27
|
-
return Promise.resolve(result).then(
|
|
28
|
-
(value) => {
|
|
2
|
+
if (typeof invoke !== 'function')
|
|
3
|
+
throw new TypeError('Core apply lifecycle requires an invoke function');
|
|
4
|
+
const succeed = typeof onSuccess === 'function' ? onSuccess : () => { };
|
|
5
|
+
const fail = (error) => {
|
|
29
6
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
7
|
+
onFailure?.(error);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// Cleanup/diagnostic failure must not replace the Core apply error.
|
|
34
11
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
12
|
+
throw error;
|
|
13
|
+
};
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
result = invoke();
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return fail(error);
|
|
20
|
+
}
|
|
21
|
+
if (result !== null
|
|
22
|
+
&& (typeof result === 'object' || typeof result === 'function')
|
|
23
|
+
&& typeof result.then === 'function') {
|
|
24
|
+
return Promise.resolve(result).then((value) => {
|
|
25
|
+
try {
|
|
26
|
+
succeed();
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return fail(error);
|
|
31
|
+
}
|
|
32
|
+
}, fail);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
succeed();
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return fail(error);
|
|
40
|
+
}
|
|
46
41
|
}
|
|
47
|
-
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function cacheWeight(value) {
|
|
2
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
3
|
+
return value.byteLength;
|
|
4
|
+
if (typeof value === 'string')
|
|
5
|
+
return Buffer.byteLength(value, 'utf8');
|
|
6
|
+
try {
|
|
7
|
+
const encoded = JSON.stringify(value);
|
|
8
|
+
return Buffer.byteLength(encoded === undefined ? String(value) : encoded, 'utf8');
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return Buffer.byteLength(String(value), 'utf8');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* LRU+TTL cache bounded by both entry count and retained bytes.
|
|
16
|
+
*
|
|
17
|
+
* Keys are normalized to strings at the ownership boundary. A rejected write
|
|
18
|
+
* never remains resident, and every eviction updates retained-byte accounting
|
|
19
|
+
* before the entry is released.
|
|
20
|
+
*/
|
|
21
|
+
export function createCache(maxEntries, ttlMsValue, options = {}) {
|
|
22
|
+
const entries = new Map();
|
|
23
|
+
const entryLimit = Math.max(0, Math.floor(Number(maxEntries) || 0));
|
|
24
|
+
const ttlMs = ttlMsValue;
|
|
25
|
+
const maxBytes = Number.isFinite(Number(options.maxBytes)) && Number(options.maxBytes) >= 0
|
|
26
|
+
? Math.floor(Number(options.maxBytes))
|
|
27
|
+
: 8 * 1024 * 1024;
|
|
28
|
+
const maxEntryBytes = Number.isFinite(Number(options.maxEntryBytes)) && Number(options.maxEntryBytes) >= 0
|
|
29
|
+
? Math.floor(Number(options.maxEntryBytes))
|
|
30
|
+
: Math.min(maxBytes, 1024 * 1024);
|
|
31
|
+
let retainedBytes = 0;
|
|
32
|
+
const remove = (key) => {
|
|
33
|
+
const entry = entries.get(key);
|
|
34
|
+
if (entry === undefined)
|
|
35
|
+
return;
|
|
36
|
+
retainedBytes = Math.max(0, retainedBytes - entry.weight);
|
|
37
|
+
entries.delete(key);
|
|
38
|
+
};
|
|
39
|
+
const evict = () => {
|
|
40
|
+
while (entries.size > entryLimit || retainedBytes > maxBytes) {
|
|
41
|
+
const oldest = entries.keys().next().value;
|
|
42
|
+
if (oldest === undefined)
|
|
43
|
+
break;
|
|
44
|
+
remove(oldest);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
get(key) {
|
|
49
|
+
const rawKey = key;
|
|
50
|
+
const entry = entries.get(rawKey);
|
|
51
|
+
if (entry === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (entry.expiresAt <= Date.now()) {
|
|
54
|
+
remove(rawKey);
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
entries.delete(rawKey);
|
|
58
|
+
entries.set(rawKey, entry);
|
|
59
|
+
return entry.value;
|
|
60
|
+
},
|
|
61
|
+
set(key, value) {
|
|
62
|
+
const normalizedKey = String(key);
|
|
63
|
+
const weight = Buffer.byteLength(normalizedKey, 'utf8') + cacheWeight(value);
|
|
64
|
+
remove(normalizedKey);
|
|
65
|
+
if (entryLimit === 0
|
|
66
|
+
|| maxBytes === 0
|
|
67
|
+
|| weight > maxEntryBytes
|
|
68
|
+
|| weight > maxBytes) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
entries.set(normalizedKey, {
|
|
72
|
+
value,
|
|
73
|
+
weight,
|
|
74
|
+
expiresAt: ttlMs <= 0 ? Infinity : Date.now() + ttlMs,
|
|
75
|
+
});
|
|
76
|
+
retainedBytes += weight;
|
|
77
|
+
evict();
|
|
78
|
+
return entries.has(normalizedKey);
|
|
79
|
+
},
|
|
80
|
+
get size() {
|
|
81
|
+
return entries.size;
|
|
82
|
+
},
|
|
83
|
+
get bytes() {
|
|
84
|
+
return retainedBytes;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
package/lib/core-primitives.js
CHANGED
|
@@ -611,77 +611,7 @@ export function extractJson(text) {
|
|
|
611
611
|
return undefined
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
|
|
615
|
-
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return value.byteLength
|
|
616
|
-
if (typeof value === 'string') return Buffer.byteLength(value, 'utf8')
|
|
617
|
-
try {
|
|
618
|
-
const encoded = JSON.stringify(value)
|
|
619
|
-
return Buffer.byteLength(encoded === undefined ? String(value) : encoded, 'utf8')
|
|
620
|
-
} catch {
|
|
621
|
-
return Buffer.byteLength(String(value), 'utf8')
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/** LRU+TTL cache bounded by BOTH entry count and retained bytes. */
|
|
626
|
-
export function createCache(maxEntries, ttlMs, options = {}) {
|
|
627
|
-
const entries = new Map()
|
|
628
|
-
const entryLimit = Math.max(0, Math.floor(Number(maxEntries) || 0))
|
|
629
|
-
const maxBytes = Number.isFinite(Number(options.maxBytes)) && Number(options.maxBytes) >= 0
|
|
630
|
-
? Math.floor(Number(options.maxBytes))
|
|
631
|
-
: 8 * 1024 * 1024
|
|
632
|
-
const maxEntryBytes = Number.isFinite(Number(options.maxEntryBytes)) && Number(options.maxEntryBytes) >= 0
|
|
633
|
-
? Math.floor(Number(options.maxEntryBytes))
|
|
634
|
-
: Math.min(maxBytes, 1024 * 1024)
|
|
635
|
-
let retainedBytes = 0
|
|
636
|
-
|
|
637
|
-
const remove = (key) => {
|
|
638
|
-
const entry = entries.get(key)
|
|
639
|
-
if (!entry) return
|
|
640
|
-
retainedBytes = Math.max(0, retainedBytes - entry.weight)
|
|
641
|
-
entries.delete(key)
|
|
642
|
-
}
|
|
643
|
-
const evict = () => {
|
|
644
|
-
while (entries.size > entryLimit || retainedBytes > maxBytes) {
|
|
645
|
-
const oldest = entries.keys().next().value
|
|
646
|
-
if (oldest === undefined) break
|
|
647
|
-
remove(oldest)
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return {
|
|
652
|
-
get(key) {
|
|
653
|
-
const entry = entries.get(key)
|
|
654
|
-
if (!entry) return undefined
|
|
655
|
-
if (entry.expiresAt <= Date.now()) {
|
|
656
|
-
remove(key)
|
|
657
|
-
return undefined
|
|
658
|
-
}
|
|
659
|
-
entries.delete(key)
|
|
660
|
-
entries.set(key, entry)
|
|
661
|
-
return entry.value
|
|
662
|
-
},
|
|
663
|
-
set(key, value) {
|
|
664
|
-
const normalizedKey = String(key)
|
|
665
|
-
const weight = Buffer.byteLength(normalizedKey, 'utf8') + cacheWeight(value)
|
|
666
|
-
remove(normalizedKey)
|
|
667
|
-
if (entryLimit === 0 || maxBytes === 0 || weight > maxEntryBytes || weight > maxBytes) return false
|
|
668
|
-
entries.set(normalizedKey, {
|
|
669
|
-
value,
|
|
670
|
-
weight,
|
|
671
|
-
expiresAt: ttlMs <= 0 ? Infinity : Date.now() + ttlMs,
|
|
672
|
-
})
|
|
673
|
-
retainedBytes += weight
|
|
674
|
-
evict()
|
|
675
|
-
return entries.has(normalizedKey)
|
|
676
|
-
},
|
|
677
|
-
get size() {
|
|
678
|
-
return entries.size
|
|
679
|
-
},
|
|
680
|
-
get bytes() {
|
|
681
|
-
return retainedBytes
|
|
682
|
-
},
|
|
683
|
-
}
|
|
684
|
-
}
|
|
614
|
+
export { cacheWeight, createCache } from './core-cache.js'
|
|
685
615
|
|
|
686
616
|
/** True when the harness llm service has a registered adapter for the provider route. */
|
|
687
617
|
export function adapterAvailable(llm, provider) {
|
|
@@ -2007,7 +1937,11 @@ async function callLmStudioNative(provider, messages, options = {}) {
|
|
|
2007
1937
|
...(typeof options.temperature === 'number' ? { temperature: options.temperature } : {}),
|
|
2008
1938
|
...(typeof options.top_p === 'number' ? { top_p: options.top_p } : {}),
|
|
2009
1939
|
}
|
|
2010
|
-
const
|
|
1940
|
+
const transport = options.providerTransport
|
|
1941
|
+
const requestFetch = typeof transport?.fetch === 'function'
|
|
1942
|
+
? (input, init) => transport.fetch(input, init, { providerName: provider.name, allowProxy: true })
|
|
1943
|
+
: fetch
|
|
1944
|
+
const response = await requestFetch(`${apiRoot}/api/v1/chat`, {
|
|
2011
1945
|
method: 'POST',
|
|
2012
1946
|
headers: { 'content-type': 'application/json' },
|
|
2013
1947
|
body: JSON.stringify(body),
|
|
@@ -2054,6 +1988,9 @@ export async function callLocalBackend(provider, messages, options = {}) {
|
|
|
2054
1988
|
return callLmStudioNative(provider, messages, {
|
|
2055
1989
|
maxTokens,
|
|
2056
1990
|
signal: options.signal,
|
|
1991
|
+
// The native LM Studio transport is a provider leg like the openai/anthropic
|
|
1992
|
+
// ones: it must use the Router-owned transport so proxy/proxyHosts apply.
|
|
1993
|
+
providerTransport: options.providerTransport,
|
|
2057
1994
|
...(typeof provider.reasoningEffort === 'string' ? { reasoningEffort: provider.reasoningEffort } : {}),
|
|
2058
1995
|
...sampling,
|
|
2059
1996
|
})
|
|
@@ -2335,77 +2272,8 @@ export async function callOpenAICompatible(provider, messages, options = {}) {
|
|
|
2335
2272
|
return text
|
|
2336
2273
|
}
|
|
2337
2274
|
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
* `llm/stream` chunk protocol and produces the final text of text blocks.
|
|
2341
|
-
* Terminal failures throw; a `max-tokens` finish returns the partial text.
|
|
2342
|
-
*/
|
|
2343
|
-
export function createChunkAssembler() {
|
|
2344
|
-
const parts = new Map()
|
|
2345
|
-
const order = []
|
|
2346
|
-
let finishKind
|
|
2347
|
-
let failure
|
|
2348
|
-
|
|
2349
|
-
const push = (chunk) => {
|
|
2350
|
-
if (!chunk || typeof chunk.type !== 'string') return
|
|
2351
|
-
switch (chunk.type) {
|
|
2352
|
-
case 'block-start': {
|
|
2353
|
-
if (!parts.has(chunk.index)) {
|
|
2354
|
-
order.push(chunk.index)
|
|
2355
|
-
parts.set(chunk.index, { type: chunk.blockType, text: '' })
|
|
2356
|
-
}
|
|
2357
|
-
break
|
|
2358
|
-
}
|
|
2359
|
-
case 'text-delta': {
|
|
2360
|
-
const part = parts.get(chunk.index)
|
|
2361
|
-
if (part) part.text += chunk.text ?? ''
|
|
2362
|
-
break
|
|
2363
|
-
}
|
|
2364
|
-
case 'reasoning-delta':
|
|
2365
|
-
case 'tool-call-delta':
|
|
2366
|
-
case 'usage':
|
|
2367
|
-
break
|
|
2368
|
-
case 'block-end': {
|
|
2369
|
-
const part = parts.get(chunk.index)
|
|
2370
|
-
if (part && chunk.block && typeof chunk.block.text === 'string') {
|
|
2371
|
-
part.text = chunk.block.text
|
|
2372
|
-
}
|
|
2373
|
-
break
|
|
2374
|
-
}
|
|
2375
|
-
case 'finish': {
|
|
2376
|
-
const reason = chunk.reason
|
|
2377
|
-
if (reason && (reason.kind === 'error' || reason.kind === 'aborted')) {
|
|
2378
|
-
failure = reason.failure
|
|
2379
|
-
}
|
|
2380
|
-
finishKind = reason && reason.kind ? reason.kind : 'stop'
|
|
2381
|
-
break
|
|
2382
|
-
}
|
|
2383
|
-
case 'error':
|
|
2384
|
-
case 'aborted':
|
|
2385
|
-
failure = chunk.failure
|
|
2386
|
-
break
|
|
2387
|
-
default:
|
|
2388
|
-
break
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
const finish = () => {
|
|
2393
|
-
if (failure) {
|
|
2394
|
-
throw new Error(failure && failure.message ? failure.message : String(failure))
|
|
2395
|
-
}
|
|
2396
|
-
if (finishKind !== undefined && finishKind !== 'stop' && finishKind !== 'max-tokens') {
|
|
2397
|
-
throw new Error(`vision call finished with "${finishKind}"`)
|
|
2398
|
-
}
|
|
2399
|
-
return order
|
|
2400
|
-
.map((index) => parts.get(index))
|
|
2401
|
-
.filter((part) => part && part.type === 'text')
|
|
2402
|
-
.map((part) => part.text)
|
|
2403
|
-
.join('')
|
|
2404
|
-
.trim()
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
return { push, finish }
|
|
2408
|
-
}
|
|
2275
|
+
import { createChunkAssembler } from './vision-chunk-assembler.js'
|
|
2276
|
+
export { createChunkAssembler }
|
|
2409
2277
|
|
|
2410
2278
|
async function visionAnswer(llm, options) {
|
|
2411
2279
|
return runWithVisionSessionAffinity(options?.sessionId, async () => {
|
|
@@ -2972,7 +2840,6 @@ export {
|
|
|
2972
2840
|
FAILURE_ADVICE,
|
|
2973
2841
|
PERSISTED_GUARD_STOP_SURFACE_ID,
|
|
2974
2842
|
imageMarker,
|
|
2975
|
-
cacheWeight,
|
|
2976
2843
|
visionAnswer,
|
|
2977
2844
|
NON_GENERATIVE_VISION_MODEL_HINTS,
|
|
2978
2845
|
VISION_MODEL_NAME_HINTS,
|