@ran-sh/dsh-crew 0.5.5 → 0.5.7
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 +14 -7
- package/README.zh.md +12 -7
- package/docs/installation.md +1 -1
- package/docs/ui-surfaces.md +65 -46
- package/lib/client.js +345 -22
- package/official-web-bridge/lib/client.js +345 -22
- package/package.json +1 -1
- package/src/client/collapsible-sections.mjs +3 -2
- package/src/client/index.tsx +227 -51
- package/src/config-readiness.mjs +56 -5
- package/src/extension-contract.mjs +9 -5
- package/src/hub/index.mjs +854 -445
- package/src/hub-client.mjs +24 -4
- package/src/hub-compatibility.mjs +23 -7
- package/src/install/install.mjs +4 -4
- package/src/install/npx-lifecycle.mjs +254 -87
- package/src/job-contracts.mjs +19 -3
- package/src/jobs.mjs +2 -4
- package/src/local-request-guard.mjs +60 -0
- package/src/mcp-runtime.mjs +7 -5
- package/src/model-routing.mjs +141 -49
- package/src/multimodal.mjs +0 -0
- package/src/official-web-bridge.mjs +242 -84
- package/src/policy.mjs +62 -31
- package/src/provider-config-scrub.mjs +71 -0
- package/src/provider-delete-adapters.mjs +424 -0
- package/src/provider-health.mjs +124 -0
- package/src/provider-inventory.mjs +132 -0
- package/src/provider-lifecycle-state.mjs +81 -0
- package/src/provider-lifecycle.mjs +214 -0
- package/src/provider-profile-store.mjs +171 -0
- package/src/readiness-matrix.mjs +11 -5
- package/src/removable-waiter.mjs +29 -0
- package/src/runtime-identity.mjs +43 -13
- package/src/server.mjs +34 -16
- package/src/status-shard.mjs +13 -2
- package/src/workflow-runtime.mjs +44 -10
- package/windows/start-dsh-crew.cmd +3 -3
- package/windows/start-dsh-crew.ps1 +49 -5
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Shared local-request trust primitives for the two loopback surfaces:
|
|
2
|
+
// the isolated 3210 hub API (src/hub/index.mjs) and the 3080→3210
|
|
3
|
+
// official-web bridge (src/official-web-bridge.mjs). One implementation keeps
|
|
4
|
+
// the two guards from drifting apart again.
|
|
5
|
+
//
|
|
6
|
+
// Trust model (see docs/ui-surfaces.md): both surfaces are loopback-only and
|
|
7
|
+
// have no bearer token by design — every process running as the local user is
|
|
8
|
+
// inside the trust boundary. The guards exist to keep off-machine browsers,
|
|
9
|
+
// DNS-rebinding pages, and cross-machine proxies out, not to authenticate
|
|
10
|
+
// local users. `localRequestCore` proves the peer is this machine (loopback
|
|
11
|
+
// socket, local Host header, browser fetch-metadata pass); each surface then
|
|
12
|
+
// layers its own Origin policy on top — the hub accepts any loopback origin
|
|
13
|
+
// because the 3080 panel calls it cross-port, while the bridge requires an
|
|
14
|
+
// exact authority match because it only serves its own same-origin client.
|
|
15
|
+
|
|
16
|
+
function isIpv4Loopback(a) { return /^127(\.\d{1,3}){3}$/.test(a); }
|
|
17
|
+
|
|
18
|
+
export function isLoopbackAddress(address) {
|
|
19
|
+
if (!address) return false;
|
|
20
|
+
const n = address.toLowerCase().split('%', 1)[0];
|
|
21
|
+
if (n === '::1' || isIpv4Loopback(n)) return true;
|
|
22
|
+
if (!n.startsWith('::ffff:')) return false;
|
|
23
|
+
return isIpv4Loopback(n.slice(7));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isLocalHostname(hostname) {
|
|
27
|
+
return hostname === 'localhost' || hostname === '::1' || hostname === '[::1]' || isIpv4Loopback(hostname);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Core of both guards: loopback peer, loopback Host header, and (when the
|
|
32
|
+
* client is a browser) a same-origin/none fetch-metadata request. Returns
|
|
33
|
+
* false for anything that cannot be proven local.
|
|
34
|
+
*/
|
|
35
|
+
export function localRequestCore(req) {
|
|
36
|
+
if (!isLoopbackAddress(req?.socket?.remoteAddress)) return false;
|
|
37
|
+
const host = typeof req?.headers?.host === 'string' ? req.headers.host.trim().toLowerCase() : '';
|
|
38
|
+
if (!host) return false;
|
|
39
|
+
let authority;
|
|
40
|
+
try { authority = new URL(`http://${host}`); } catch { return false; }
|
|
41
|
+
if (!isLocalHostname(authority.hostname.toLowerCase())) return false;
|
|
42
|
+
const fetchSite = String(req?.headers?.['sec-fetch-site'] ?? '').toLowerCase();
|
|
43
|
+
if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') return false;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Hub Origin policy: an Origin, when present, must name a loopback host. */
|
|
48
|
+
export function originLoopback(originHeader) {
|
|
49
|
+
if (typeof originHeader !== 'string') return false;
|
|
50
|
+
let parsed;
|
|
51
|
+
try { parsed = new URL(originHeader); } catch { return false; }
|
|
52
|
+
return isLocalHostname(parsed.hostname.toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Bridge Origin policy: the Origin authority must equal the request authority. */
|
|
56
|
+
export function originAuthorityMatches(originHeader, host) {
|
|
57
|
+
if (!originLoopback(originHeader)) return false;
|
|
58
|
+
const parsed = new URL(originHeader);
|
|
59
|
+
return parsed.host.toLowerCase() === String(host).trim().toLowerCase();
|
|
60
|
+
}
|
package/src/mcp-runtime.mjs
CHANGED
|
@@ -69,8 +69,9 @@ export function attemptFromView(view, spec) {
|
|
|
69
69
|
modelClassHint: spec.model_class_hint ?? null,
|
|
70
70
|
strategy: source ?? 'transport-selection',
|
|
71
71
|
candidateSet: logicalAttempt > 0 ? 'escalation' : 'primary',
|
|
72
|
-
provider: view?.provider ?? null,
|
|
73
|
-
model: view?.model ?? null,
|
|
72
|
+
provider: view?.provider ?? null,
|
|
73
|
+
model: view?.model ?? null,
|
|
74
|
+
execution_context: view?.execution_context ?? null,
|
|
74
75
|
source: source ?? 'transport-selection',
|
|
75
76
|
escalationReason: spec.escalation_reason ?? null,
|
|
76
77
|
});
|
|
@@ -86,9 +87,10 @@ export function attemptFromView(view, spec) {
|
|
|
86
87
|
// `view.attempt` may be an adapter routing-attempt (see below); the
|
|
87
88
|
// workflow's logical attempt number is always the spec value.
|
|
88
89
|
attempt: logicalAttempt,
|
|
89
|
-
provider: view?.provider ?? null,
|
|
90
|
-
model: view?.model ?? null,
|
|
91
|
-
|
|
90
|
+
provider: view?.provider ?? null,
|
|
91
|
+
model: view?.model ?? null,
|
|
92
|
+
execution_context: view?.execution_context ?? null,
|
|
93
|
+
selection_source: source,
|
|
92
94
|
selection_trace: selectionTrace,
|
|
93
95
|
status: view?.status ?? 'failed',
|
|
94
96
|
result: view?.result ?? null,
|
package/src/model-routing.mjs
CHANGED
|
@@ -26,13 +26,36 @@ export const MODEL_SELECTION_REASON_CODES = Object.freeze({
|
|
|
26
26
|
PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
|
|
27
27
|
PREFERRED_MODEL_UNAVAILABLE: 'PREFERRED_MODEL_UNAVAILABLE',
|
|
28
28
|
PREFERRED_MODEL_AMBIGUOUS: 'PREFERRED_MODEL_AMBIGUOUS',
|
|
29
|
-
ADAPTIVE_DEPRIORITIZED: 'ADAPTIVE_DEPRIORITIZED',
|
|
30
|
-
|
|
29
|
+
ADAPTIVE_DEPRIORITIZED: 'ADAPTIVE_DEPRIORITIZED',
|
|
30
|
+
PROVIDER_TOMBSTONED: 'PROVIDER_TOMBSTONED',
|
|
31
|
+
CREDENTIAL_MISSING: 'CREDENTIAL_MISSING',
|
|
32
|
+
QUOTA_EXHAUSTED: 'QUOTA_EXHAUSTED',
|
|
33
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
34
|
+
PROBE_TIMEOUT: 'PROBE_TIMEOUT',
|
|
35
|
+
PROVIDER_INTERNAL_ERROR: 'PROVIDER_INTERNAL_ERROR',
|
|
36
|
+
HARNESS_DEFAULT_INVALID: 'HARNESS_DEFAULT_INVALID',
|
|
31
37
|
HARNESS_DEFAULT_PROVIDER_UNAVAILABLE: 'HARNESS_DEFAULT_PROVIDER_UNAVAILABLE',
|
|
32
38
|
PRIMARY_CANDIDATES_EXHAUSTED: 'PRIMARY_CANDIDATES_EXHAUSTED',
|
|
33
39
|
ESCALATION_CANDIDATES_EXHAUSTED: 'ESCALATION_CANDIDATES_EXHAUSTED',
|
|
34
|
-
NO_AVAILABLE_MODEL: 'NO_AVAILABLE_MODEL',
|
|
35
|
-
});
|
|
40
|
+
NO_AVAILABLE_MODEL: 'NO_AVAILABLE_MODEL',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const HEALTH_BLOCK_REASONS = Object.freeze({
|
|
44
|
+
'credential-missing': MODEL_SELECTION_REASON_CODES.CREDENTIAL_MISSING,
|
|
45
|
+
'quota-exhausted': MODEL_SELECTION_REASON_CODES.QUOTA_EXHAUSTED,
|
|
46
|
+
'rate-limited': MODEL_SELECTION_REASON_CODES.RATE_LIMITED,
|
|
47
|
+
timeout: MODEL_SELECTION_REASON_CODES.PROBE_TIMEOUT,
|
|
48
|
+
'internal-error': MODEL_SELECTION_REASON_CODES.PROVIDER_INTERNAL_ERROR,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const BLOCKED_MODEL_CODES = Object.freeze({
|
|
52
|
+
[MODEL_SELECTION_REASON_CODES.CREDENTIAL_MISSING]: 'MODEL_BLOCKED_CREDENTIAL',
|
|
53
|
+
[MODEL_SELECTION_REASON_CODES.QUOTA_EXHAUSTED]: 'MODEL_BLOCKED_QUOTA',
|
|
54
|
+
[MODEL_SELECTION_REASON_CODES.RATE_LIMITED]: 'MODEL_BLOCKED_RATE_LIMIT',
|
|
55
|
+
[MODEL_SELECTION_REASON_CODES.PROBE_TIMEOUT]: 'MODEL_BLOCKED_TIMEOUT',
|
|
56
|
+
[MODEL_SELECTION_REASON_CODES.PROVIDER_INTERNAL_ERROR]: 'MODEL_BLOCKED_PROVIDER',
|
|
57
|
+
[MODEL_SELECTION_REASON_CODES.PROVIDER_TOMBSTONED]: 'MODEL_BLOCKED_TOMBSTONED',
|
|
58
|
+
});
|
|
36
59
|
|
|
37
60
|
export function normalizeModelRef(raw) {
|
|
38
61
|
if (!raw || typeof raw !== 'object') return null;
|
|
@@ -101,7 +124,7 @@ function traceBase({
|
|
|
101
124
|
};
|
|
102
125
|
}
|
|
103
126
|
|
|
104
|
-
function candidateDecision(ref, source, status, { reasonCode, advertised } = {}) {
|
|
127
|
+
function candidateDecision(ref, source, status, { reasonCode, advertised } = {}) {
|
|
105
128
|
return {
|
|
106
129
|
provider: ref?.provider ?? null,
|
|
107
130
|
model: ref?.model ?? null,
|
|
@@ -110,7 +133,25 @@ function candidateDecision(ref, source, status, { reasonCode, advertised } = {})
|
|
|
110
133
|
...(reasonCode ? { reason_code: reasonCode } : {}),
|
|
111
134
|
...(advertised === false ? { advertised: false } : {}),
|
|
112
135
|
};
|
|
113
|
-
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function healthAdmissionReason(ref, { healthStore, healthGate, tombstones } = {}) {
|
|
139
|
+
if (tombstones?.[ref?.provider] === 'absent') return MODEL_SELECTION_REASON_CODES.PROVIDER_TOMBSTONED;
|
|
140
|
+
if (healthGate !== 'hard-failures' || typeof healthStore?.get !== 'function') return null;
|
|
141
|
+
const observation = healthStore.get(ref?.provider, ref?.model);
|
|
142
|
+
if (observation?.fresh !== true) return null;
|
|
143
|
+
return HEALTH_BLOCK_REASONS[observation.state] ?? null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function blockedSelection(trace, ref, source, reasonCode) {
|
|
147
|
+
trace.fallback_reason = reasonCode;
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
code: BLOCKED_MODEL_CODES[reasonCode] ?? 'MODEL_BLOCKED_PROVIDER',
|
|
151
|
+
message: `Model ${ref?.provider ?? 'unknown'}/${ref?.model ?? 'unknown'} is blocked by ${reasonCode}.`,
|
|
152
|
+
selection_trace: trace,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
114
155
|
|
|
115
156
|
function selectTrace(trace, ref, source, { advertised, fallbackReason } = {}) {
|
|
116
157
|
trace.ordered_candidates.push(candidateDecision(ref, source, 'selected', { advertised }));
|
|
@@ -198,7 +239,7 @@ export function enrichSelectionTrace(trace, {
|
|
|
198
239
|
return base;
|
|
199
240
|
}
|
|
200
241
|
|
|
201
|
-
export function resolveWorkerModel({
|
|
242
|
+
export function resolveWorkerModel({
|
|
202
243
|
tier,
|
|
203
244
|
priority,
|
|
204
245
|
priorityConfigured = false,
|
|
@@ -207,9 +248,13 @@ export function resolveWorkerModel({
|
|
|
207
248
|
fallback = 'harness-default',
|
|
208
249
|
preferredModelId = DEFAULT_TIER_MODEL_PREFERENCES[tier],
|
|
209
250
|
traceContext = {},
|
|
210
|
-
adaptive,
|
|
211
|
-
adaptiveHealth,
|
|
212
|
-
|
|
251
|
+
adaptive,
|
|
252
|
+
adaptiveHealth,
|
|
253
|
+
healthStore,
|
|
254
|
+
healthGate,
|
|
255
|
+
allowFallback = true,
|
|
256
|
+
tombstones,
|
|
257
|
+
} = {}) {
|
|
213
258
|
const providers = providerMap(catalog);
|
|
214
259
|
const normalizedPriority = normalizeModelPriority(priority);
|
|
215
260
|
const prioritySource = typeof traceContext.prioritySource === 'string' && traceContext.prioritySource
|
|
@@ -230,17 +275,25 @@ export function resolveWorkerModel({
|
|
|
230
275
|
rankForTrace(trace, normalizedPriority, { adaptive, adaptiveHealth, explicitPriority: true });
|
|
231
276
|
}
|
|
232
277
|
|
|
233
|
-
for (let index = 0; index < normalizedPriority.length; index++) {
|
|
234
|
-
const ref = normalizedPriority[index];
|
|
235
|
-
const provider = providers.get(ref.provider);
|
|
278
|
+
for (let index = 0; index < normalizedPriority.length; index++) {
|
|
279
|
+
const ref = normalizedPriority[index];
|
|
280
|
+
const provider = providers.get(ref.provider);
|
|
236
281
|
if (!provider) {
|
|
237
282
|
trace.ordered_candidates.push(candidateDecision(ref, prioritySource, 'skipped', {
|
|
238
283
|
reasonCode: MODEL_SELECTION_REASON_CODES.PROVIDER_UNAVAILABLE,
|
|
239
|
-
}));
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
const advertised = (provider.models ?? []).some((model) => model?.id === ref.model);
|
|
243
|
-
|
|
284
|
+
}));
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const advertised = (provider.models ?? []).some((model) => model?.id === ref.model);
|
|
288
|
+
const healthReason = healthAdmissionReason(ref, { healthStore, healthGate, tombstones });
|
|
289
|
+
if (healthReason) {
|
|
290
|
+
trace.ordered_candidates.push(candidateDecision(ref, prioritySource, 'skipped', {
|
|
291
|
+
reasonCode: healthReason, advertised,
|
|
292
|
+
}));
|
|
293
|
+
if (allowFallback === false) return blockedSelection(trace, ref, prioritySource, healthReason);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
selectTrace(trace, ref, prioritySource, { advertised });
|
|
244
297
|
return {
|
|
245
298
|
ok: true,
|
|
246
299
|
...ref,
|
|
@@ -259,24 +312,47 @@ export function resolveWorkerModel({
|
|
|
259
312
|
if ((provider.models ?? []).some((model) => model?.id === preferredModelId)) {
|
|
260
313
|
matches.push({ provider: provider.id, model: preferredModelId });
|
|
261
314
|
}
|
|
262
|
-
}
|
|
263
|
-
if (matches.length === 1) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
315
|
+
}
|
|
316
|
+
if (matches.length === 1) {
|
|
317
|
+
const preferred = matches[0];
|
|
318
|
+
const healthReason = healthAdmissionReason(preferred, { healthStore, healthGate, tombstones });
|
|
319
|
+
if (healthReason) {
|
|
320
|
+
trace.ordered_candidates.push(candidateDecision(preferred, 'preferred-default', 'skipped', {
|
|
321
|
+
reasonCode: healthReason,
|
|
322
|
+
}));
|
|
323
|
+
if (allowFallback === false) return blockedSelection(trace, preferred, 'preferred-default', healthReason);
|
|
324
|
+
} else {
|
|
325
|
+
rankForTrace(trace, matches, { adaptive, adaptiveHealth });
|
|
326
|
+
selectTrace(trace, preferred, 'preferred-default');
|
|
327
|
+
return { ok: true, ...preferred, source: 'preferred-default', selection_trace: trace };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (matches.length > 1) {
|
|
331
|
+
const availableMatches = matches.filter((candidate) => {
|
|
332
|
+
const healthReason = healthAdmissionReason(candidate, { healthStore, healthGate, tombstones });
|
|
333
|
+
if (!healthReason) return true;
|
|
334
|
+
trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
|
|
335
|
+
reasonCode: healthReason,
|
|
336
|
+
}));
|
|
337
|
+
return false;
|
|
338
|
+
});
|
|
339
|
+
if (availableMatches.length === 0) {
|
|
340
|
+
const blocked = matches.find((candidate) => healthAdmissionReason(candidate, { healthStore, healthGate, tombstones }));
|
|
341
|
+
if (blocked && allowFallback === false) {
|
|
342
|
+
return blockedSelection(trace, blocked, 'preferred-default', healthAdmissionReason(blocked, { healthStore, healthGate, tombstones }));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const preferredProvider = normalizeModelRef(harnessDefault)?.provider;
|
|
346
|
+
const deterministicMatch = availableMatches.find((candidate) => candidate.provider === preferredProvider) ?? null;
|
|
347
|
+
const baseline = deterministicMatch
|
|
348
|
+
? [deterministicMatch, ...availableMatches.filter((candidate) => candidate.provider !== deterministicMatch.provider)]
|
|
349
|
+
: availableMatches;
|
|
350
|
+
const ranked = rankForTrace(trace, baseline, { adaptive, adaptiveHealth });
|
|
351
|
+
const adaptiveChoice = ranked.trace.decision_supported ? ranked.candidates[0] : null;
|
|
352
|
+
const match = adaptiveChoice ?? deterministicMatch;
|
|
353
|
+
if (match) {
|
|
354
|
+
const decisionOrder = adaptiveChoice ? ranked.candidates : availableMatches;
|
|
355
|
+
for (const candidate of decisionOrder) {
|
|
280
356
|
if (candidate.provider === match.provider && candidate.model === match.model) continue;
|
|
281
357
|
trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
|
|
282
358
|
reasonCode: adaptiveChoice
|
|
@@ -318,20 +394,28 @@ export function resolveWorkerModel({
|
|
|
318
394
|
reasonCode: MODEL_SELECTION_REASON_CODES.HARNESS_DEFAULT_PROVIDER_UNAVAILABLE,
|
|
319
395
|
}));
|
|
320
396
|
} else {
|
|
321
|
-
const fallbackReason = traceContext.candidateSet === 'escalation'
|
|
322
|
-
? MODEL_SELECTION_REASON_CODES.ESCALATION_CANDIDATES_EXHAUSTED
|
|
323
|
-
: MODEL_SELECTION_REASON_CODES.PRIMARY_CANDIDATES_EXHAUSTED;
|
|
324
|
-
|
|
325
|
-
|
|
397
|
+
const fallbackReason = traceContext.candidateSet === 'escalation'
|
|
398
|
+
? MODEL_SELECTION_REASON_CODES.ESCALATION_CANDIDATES_EXHAUSTED
|
|
399
|
+
: MODEL_SELECTION_REASON_CODES.PRIMARY_CANDIDATES_EXHAUSTED;
|
|
400
|
+
const healthReason = healthAdmissionReason(defaultRef, { healthStore, healthGate, tombstones });
|
|
401
|
+
if (healthReason) {
|
|
402
|
+
trace.ordered_candidates.push(candidateDecision(defaultRef, 'harness-default', 'skipped', {
|
|
403
|
+
reasonCode: healthReason,
|
|
404
|
+
}));
|
|
405
|
+
if (allowFallback === false) return blockedSelection(trace, defaultRef, 'harness-default', healthReason);
|
|
406
|
+
} else {
|
|
407
|
+
selectTrace(trace, defaultRef, 'harness-default', { fallbackReason });
|
|
408
|
+
return {
|
|
326
409
|
ok: true,
|
|
327
410
|
...defaultRef,
|
|
328
411
|
source: 'harness-default',
|
|
329
412
|
...(typeof harnessDefault.reasoningEffort === 'string' && harnessDefault.reasoningEffort
|
|
330
413
|
? { reasoningEffort: harnessDefault.reasoningEffort }
|
|
331
414
|
: {}),
|
|
332
|
-
selection_trace: trace,
|
|
333
|
-
};
|
|
334
|
-
|
|
415
|
+
selection_trace: trace,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
335
419
|
}
|
|
336
420
|
trace.fallback_reason = MODEL_SELECTION_REASON_CODES.NO_AVAILABLE_MODEL;
|
|
337
421
|
return {
|
|
@@ -351,14 +435,18 @@ export function resolveWorkerModel({
|
|
|
351
435
|
* The output shape matches resolveWorkerModel (provider/model/source/...) plus
|
|
352
436
|
* role + attempt so selection provenance lands in job metadata.
|
|
353
437
|
*/
|
|
354
|
-
export function resolveModel({
|
|
438
|
+
export function resolveModel({
|
|
355
439
|
role = 'worker',
|
|
356
440
|
attempt = 0,
|
|
357
441
|
policy,
|
|
358
442
|
catalog,
|
|
359
443
|
harnessDefault,
|
|
360
|
-
adaptiveHealth,
|
|
361
|
-
|
|
444
|
+
adaptiveHealth,
|
|
445
|
+
healthStore,
|
|
446
|
+
healthGate,
|
|
447
|
+
allowFallback = true,
|
|
448
|
+
tombstones,
|
|
449
|
+
} = {}) {
|
|
362
450
|
const p = policy && typeof policy === 'object' ? policy : {};
|
|
363
451
|
const escalated = Number.isInteger(attempt) && attempt > 0;
|
|
364
452
|
const candidates = escalated ? p.escalation_priority : p.priority;
|
|
@@ -376,8 +464,12 @@ export function resolveModel({
|
|
|
376
464
|
harnessDefault,
|
|
377
465
|
fallback: p.fallback === 'harness-default' ? 'harness-default' : 'harness-default',
|
|
378
466
|
preferredModelId,
|
|
379
|
-
adaptive: p.adaptive,
|
|
380
|
-
adaptiveHealth,
|
|
467
|
+
adaptive: p.adaptive,
|
|
468
|
+
adaptiveHealth,
|
|
469
|
+
healthStore,
|
|
470
|
+
healthGate,
|
|
471
|
+
allowFallback,
|
|
472
|
+
tombstones,
|
|
381
473
|
traceContext: {
|
|
382
474
|
role,
|
|
383
475
|
logicalAttempt: attempt,
|
package/src/multimodal.mjs
CHANGED
|
Binary file
|