dsh-auto-model-routing 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/docs/SCREENSHOTS.md +2 -2
- package/docs/auto-routing.png +0 -0
- package/docs/settings.png +0 -0
- package/lib/client/index.d.ts +7 -1
- package/lib/client/index.js +47 -8
- package/lib/client.js +61 -41
- package/lib/index.js +56 -43
- package/lib/policy-host.d.ts +13 -2
- package/lib/policy-host.js +105 -85
- package/lib/policy-store.d.ts +14 -10
- package/lib/policy-store.js +27 -4
- package/lib/routing.d.ts +33 -9
- package/lib/routing.js +39 -28
- package/lib/settings-host.d.ts +2 -3
- package/lib/settings-host.js +19 -14
- package/lib/strategies/claude.d.ts +0 -5
- package/lib/strategies/claude.js +0 -1
- package/lib/strategies/codex.d.ts +1 -6
- package/lib/strategies/codex.js +1 -2
- package/lib/strategies/index.d.ts +2 -6
- package/lib/strategies/index.js +16 -23
- package/lib/strategies/types.d.ts +1 -2
- package/lib/subagent-tool.d.ts +13 -1
- package/lib/subagent-tool.js +22 -19
- package/package.json +1 -1
- package/screenshots.json +2 -2
- package/skills/subagent-model-routing/SKILL.md +5 -5
- package/docs/auto-routing.gif +0 -0
- package/docs/settings.gif +0 -0
package/lib/policy-host.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
3
|
-
import {
|
|
4
|
-
import { choosePolicyRoute, policyEffort, modelFamily, validatePolicyRoute } from './routing.js';
|
|
3
|
+
import { chooseChildRoute, policyEffort, validatePolicyRoute } from './routing.js';
|
|
5
4
|
import { registry as builtin } from './strategies/index.js';
|
|
6
5
|
import { userInput } from './input.js';
|
|
7
6
|
import { buildState, previousTurn } from './context.js';
|
|
@@ -38,37 +37,67 @@ export function policyHost(ctx, deps) {
|
|
|
38
37
|
latest.delete(agent.id);
|
|
39
38
|
applied.delete(agent.id);
|
|
40
39
|
};
|
|
40
|
+
const childEpochs = new Map();
|
|
41
|
+
const notices = new Map();
|
|
42
|
+
let noticeId = 0;
|
|
43
|
+
/** Visible fallback: the child request runs with DSH defaults, the root learns why. */
|
|
44
|
+
const fallback = (record, reason) => {
|
|
45
|
+
const next = { childId: record.childId, parentId: record.parentId, rootId: record.rootId, path: record.path, explicit: record.explicit,
|
|
46
|
+
...(record.family ? { family: record.family } : {}), ...(record.parentSelection ? { parentSelection: record.parentSelection } : {}), status: 'unsupported', reason };
|
|
47
|
+
deps.store.set(next);
|
|
48
|
+
pending.delete(record.childId);
|
|
49
|
+
childEpochs.delete(record.childId);
|
|
50
|
+
notices.set(record.rootId, { id: ++noticeId, childId: record.childId, reason, at: Date.now() });
|
|
51
|
+
ctx.logger.warn(`auto-model-routing: child ${record.childId} not routed (${reason}); running with DSH defaults`);
|
|
52
|
+
return next;
|
|
53
|
+
};
|
|
54
|
+
const capturedAllowlist = (parentId) => {
|
|
55
|
+
const events = ctx.agents.get(SessionId(parentId))?.session.snapshotEvents() ?? [];
|
|
56
|
+
const captured = events.find(event => event.type === 'subagent/model-selection-policy');
|
|
57
|
+
if (!captured)
|
|
58
|
+
return undefined;
|
|
59
|
+
const allowed = captured.data.allowedModels;
|
|
60
|
+
return Array.isArray(allowed) ? allowed : [];
|
|
61
|
+
};
|
|
41
62
|
ctx.on('tools/execute', async (exec, next) => {
|
|
42
|
-
if (!['subagent', '
|
|
63
|
+
if (!['subagent', 'spawn_teammate'].includes(exec.name) || !exec.agent)
|
|
64
|
+
return next();
|
|
65
|
+
const args = isRecord(exec.arguments) ? exec.arguments : {};
|
|
66
|
+
// Fork copies the parent: AUTO does not manage it and opens no binding window.
|
|
67
|
+
if (exec.name === 'spawn_teammate' && args.context === 'fork')
|
|
43
68
|
return next();
|
|
44
69
|
const root = rootOf(exec.agent);
|
|
45
70
|
if (!root || !enabled(root.id))
|
|
46
71
|
return next();
|
|
47
|
-
const
|
|
48
|
-
const path = exec.name === 'subagent_fork' || (exec.name === 'spawn_teammate' && args.context === 'fork') ? 'fork' : exec.name === 'spawn_teammate' ? 'teammate' : 'ordinary';
|
|
49
|
-
if (path === 'fork')
|
|
50
|
-
throw new Error('unsupported:fork 的真实历史兼容尚未验证;请暂停,改用 fresh 创建,或由用户切到 MANUAL 后继承。');
|
|
51
|
-
const provider = args.provider ?? exec.agent.session.requestHeader()?.config.provider ?? exec.agent.options.provider;
|
|
52
|
-
if (typeof provider !== 'string' || !registry.hasProvider(provider))
|
|
53
|
-
throw new Error('unsupported:无法验证 provider;暂停委派并询问用户');
|
|
54
|
-
const parentModel = exec.agent.session.requestHeader()?.config.model ?? exec.agent.options.model;
|
|
55
|
-
const family = modelFamily(provider, typeof args.model === 'string' ? args.model : parentModel ?? '', registry);
|
|
56
|
-
const explicit = { provider,
|
|
72
|
+
const explicit = { ...(typeof args.provider === 'string' && args.provider ? { provider: args.provider } : {}),
|
|
57
73
|
...(typeof args.model === 'string' ? { model: args.model } : {}),
|
|
58
74
|
...(typeof args.reasoning_effort === 'string' ? { reasoningEffort: args.reasoning_effort } : {}) };
|
|
59
|
-
return delegation.run({ parentId: exec.agent.id, rootId: root.id, path
|
|
75
|
+
return delegation.run({ parentId: exec.agent.id, rootId: root.id, path: exec.name === 'spawn_teammate' ? 'teammate' : 'ordinary', explicit }, next);
|
|
60
76
|
});
|
|
61
|
-
ctx.on('agent/created', ({ agent }) => {
|
|
62
|
-
if (agent.session.header.origin !== 'subagent')
|
|
77
|
+
ctx.on('agent/created', ({ agent, source }) => {
|
|
78
|
+
if (agent.session.header.origin !== 'subagent' || source !== 'startup')
|
|
63
79
|
return;
|
|
64
80
|
const binding = delegation.getStore();
|
|
65
|
-
|
|
81
|
+
// shortcut: same-window plugin children under this parent are indistinguishable (accepted 2026-10-28), until DSH exposes tool-call→child identity.
|
|
82
|
+
if (!binding || binding.parentId !== agent.session.header.parentSession || !enabled(binding.rootId) || deps.store.wasBound(agent.id))
|
|
66
83
|
return;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
const parent = ctx.agents.get(SessionId(binding.parentId));
|
|
85
|
+
if (!parent)
|
|
86
|
+
return;
|
|
87
|
+
const config = parent.session.requestHeader()?.config;
|
|
88
|
+
const provider = config?.provider ?? parent.options.provider;
|
|
89
|
+
const model = config?.model ?? parent.options.model;
|
|
90
|
+
const effort = config?.reasoningEffort ?? parent.options.reasoningEffort;
|
|
91
|
+
const routeProvider = binding.explicit.provider ?? provider;
|
|
92
|
+
if (typeof provider !== 'string' || !provider || typeof routeProvider !== 'string' || !routeProvider)
|
|
93
|
+
return;
|
|
94
|
+
const family = registry.familyOfMainModel(binding.explicit.model ?? (typeof model === 'string' ? model : undefined))?.id;
|
|
95
|
+
const record = { childId: agent.id, parentId: binding.parentId, rootId: binding.rootId, path: binding.path, status: 'recommended',
|
|
96
|
+
explicit: { ...binding.explicit, provider: routeProvider }, ...(family ? { family } : {}),
|
|
97
|
+
parentSelection: { provider, ...(typeof model === 'string' ? { model } : {}), ...(typeof effort === 'string' ? { reasoningEffort: effort } : {}) } };
|
|
98
|
+
deps.store.create(record);
|
|
71
99
|
pending.set(agent.id, record);
|
|
100
|
+
childEpochs.set(agent.id, epoch(binding.rootId));
|
|
72
101
|
return undefined;
|
|
73
102
|
});
|
|
74
103
|
const requireBindingRecord = (agent, record) => {
|
|
@@ -127,74 +156,66 @@ export function policyHost(ctx, deps) {
|
|
|
127
156
|
}
|
|
128
157
|
let record = pending.get(agent.id) ?? deps.store.get(agent.id);
|
|
129
158
|
requireBindingRecord(agent, record);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (root && enabled(root.id))
|
|
133
|
-
throw new Error('unsupported:创建没有官方工具身份绑定;该子请求未路由。请暂停并改用受支持的创建路径,或由用户切到 MANUAL 后继承。');
|
|
159
|
+
// Unbound children (plugin-created, MANUAL-created, fork) pass through untouched.
|
|
160
|
+
if (!record)
|
|
134
161
|
return decision;
|
|
135
|
-
}
|
|
136
162
|
if (record.parentId !== agent.session.header.parentSession)
|
|
137
163
|
throw new Error('子会话身份不匹配');
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
164
|
+
if (record.route || record.status !== 'recommended')
|
|
165
|
+
return decision;
|
|
166
|
+
if (!pending.has(agent.id)) {
|
|
167
|
+
fallback(record, 'interrupted');
|
|
168
|
+
return decision;
|
|
141
169
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
if (result.kind === 'cancelled') {
|
|
160
|
-
saveFailure('failed', '初始判断已取消');
|
|
161
|
-
throw new Error('子任务判断已取消');
|
|
162
|
-
}
|
|
163
|
-
if (result.kind === 'failed') {
|
|
164
|
-
saveFailure('failed', result.category);
|
|
165
|
-
throw new Error(`Jev 失败:${CATEGORY_HINTS[result.category]};暂停委派并询问用户`);
|
|
166
|
-
}
|
|
167
|
-
if (result.capability === 'insufficient_context') {
|
|
168
|
-
saveFailure('needs-context', '任务目标或范围不足');
|
|
169
|
-
throw new Error('needs-context:请补充任务目标与范围');
|
|
170
|
-
}
|
|
171
|
-
measure = result;
|
|
172
|
-
capability = result.capability ?? 'bounded';
|
|
173
|
-
depth = result.depth;
|
|
170
|
+
const startedEpoch = childEpochs.get(agent.id) ?? epoch(record.rootId);
|
|
171
|
+
const current = () => enabled(record.rootId) && epoch(record.rootId) === startedEpoch;
|
|
172
|
+
if (!current()) {
|
|
173
|
+
fallback(record, 'mode-changed');
|
|
174
|
+
return decision;
|
|
175
|
+
}
|
|
176
|
+
const explicit = record.explicit;
|
|
177
|
+
let measure;
|
|
178
|
+
let capability = 'bounded', depth = 'baseline';
|
|
179
|
+
if (!explicit.model || !explicit.reasoningEffort) {
|
|
180
|
+
// This is the exact child's entering message, not a parent-history/prompt similarity match.
|
|
181
|
+
const text = userInput(decision.messages).text;
|
|
182
|
+
const result = await deps.jev.judgePolicy(buildState({ text }), payload.signal, 'child', explicit.model ? 'depth' : explicit.reasoningEffort ? 'capability' : 'both');
|
|
183
|
+
if (!current()) {
|
|
184
|
+
fallback(record, 'mode-changed');
|
|
185
|
+
return decision;
|
|
174
186
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const events = parent?.session.snapshotEvents() ?? [];
|
|
179
|
-
const captured = events.find(event => event.type === 'subagent/model-selection-policy');
|
|
180
|
-
if (captured) {
|
|
181
|
-
const allowed = captured.data.allowedModels;
|
|
182
|
-
if (!Array.isArray(allowed) || !allowed.some(item => item.provider === route.provider && item.model === route.model))
|
|
183
|
-
throw new Error('路由不在父会话创建时的官方允许列表中;请新建会话');
|
|
184
|
-
}
|
|
185
|
-
if (!enabled(record.rootId) || payload.signal.aborted)
|
|
186
|
-
throw new Error('创建已取消或模式已变化');
|
|
187
|
-
record = { ...record, route, status: 'recommended', decision: { id: ++decisionId, source: 'subagent', tier: capability, depth, capability, effort: route.reasoningEffort, model: route.model, provider: route.provider, status: 'recommended', ms: measure?.ms ?? 0, decisionProvider: measure?.decisionProvider ?? 'explicit', decisionModel: measure?.decisionModel ?? '未调用', ...(measure?.confidence !== undefined ? { confidence: measure.confidence } : {}), ...(measure?.capabilityConfidence !== undefined ? { capabilityConfidence: measure.capabilityConfidence } : {}), ...(measure?.probabilities ? { probabilities: measure.probabilities } : {}), ...(measure?.capabilityProbabilities ? { capabilityProbabilities: measure.capabilityProbabilities } : {}) } };
|
|
188
|
-
deps.store.set(record);
|
|
189
|
-
pending.delete(agent.id);
|
|
187
|
+
if (result.kind === 'cancelled') {
|
|
188
|
+
fallback(record, 'jev-cancelled');
|
|
189
|
+
return decision;
|
|
190
190
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
if (result.kind === 'failed') {
|
|
192
|
+
fallback(record, `jev-${result.category}`);
|
|
193
|
+
return decision;
|
|
194
|
+
}
|
|
195
|
+
if (result.capability === 'insufficient_context') {
|
|
196
|
+
fallback(record, 'needs-context');
|
|
197
|
+
return decision;
|
|
194
198
|
}
|
|
199
|
+
measure = result;
|
|
200
|
+
capability = result.capability ?? 'bounded';
|
|
201
|
+
depth = result.depth;
|
|
202
|
+
}
|
|
203
|
+
const catalog = await deps.catalog(explicit.provider);
|
|
204
|
+
if (!current()) {
|
|
205
|
+
fallback(record, 'mode-changed');
|
|
206
|
+
return decision;
|
|
207
|
+
}
|
|
208
|
+
const parent = record.parentSelection ?? { provider: explicit.provider };
|
|
209
|
+
const chosen = chooseChildRoute({ provider: explicit.provider, parent, explicit, capability, depth, catalog, cap: deps.settings().effortCap, captured: capturedAllowlist(record.parentId) }, registry);
|
|
210
|
+
if ('fallback' in chosen) {
|
|
211
|
+
fallback(record, chosen.fallback);
|
|
212
|
+
return decision;
|
|
195
213
|
}
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
const route = chosen.route;
|
|
215
|
+
record = { ...record, route, status: 'recommended', decision: { id: ++decisionId, source: 'subagent', tier: capability, depth, capability, effort: route.reasoningEffort, model: route.model, provider: route.provider, status: 'recommended', ms: measure?.ms ?? 0, decisionProvider: measure?.decisionProvider ?? 'explicit', decisionModel: measure?.decisionModel ?? '未调用', ...(measure?.confidence !== undefined ? { confidence: measure.confidence } : {}), ...(measure?.capabilityConfidence !== undefined ? { capabilityConfidence: measure.capabilityConfidence } : {}), ...(measure?.probabilities ? { probabilities: measure.probabilities } : {}), ...(measure?.capabilityProbabilities ? { capabilityProbabilities: measure.capabilityProbabilities } : {}) } };
|
|
216
|
+
deps.store.set(record);
|
|
217
|
+
pending.delete(agent.id);
|
|
218
|
+
childEpochs.delete(agent.id);
|
|
198
219
|
return decision;
|
|
199
220
|
});
|
|
200
221
|
ctx.on('agent/request', async (payload, next) => {
|
|
@@ -245,16 +266,14 @@ export function policyHost(ctx, deps) {
|
|
|
245
266
|
}
|
|
246
267
|
const record = deps.store.get(agent.id);
|
|
247
268
|
requireBindingRecord(agent, record);
|
|
248
|
-
if (record && !record.route)
|
|
249
|
-
throw new Error('初始路由未完成;不静默继承');
|
|
250
269
|
if (!record?.route)
|
|
251
270
|
return config;
|
|
252
271
|
if (record.status === 'failed')
|
|
253
272
|
throw new Error('实际应用失败;暂停子请求并询问用户');
|
|
254
273
|
if (record.parentId !== agent.session.header.parentSession)
|
|
255
274
|
throw new Error('决策身份不匹配');
|
|
256
|
-
// Existing routed instances keep their initial route
|
|
257
|
-
validatePolicyRoute(await deps.catalog(), record.route.provider, record.route.model, record.route.reasoningEffort, deps.settings().effortCap,
|
|
275
|
+
// Existing routed instances keep their initial route; live gates are re-checked on every request.
|
|
276
|
+
validatePolicyRoute(await deps.catalog(record.route.provider), record.route.provider, record.route.model, record.route.reasoningEffort, deps.settings().effortCap, capturedAllowlist(record.parentId));
|
|
258
277
|
return { ...config, ...record.route };
|
|
259
278
|
}, { prepend: true });
|
|
260
279
|
const confirmMainRequest = (session, requestSeq, turn) => {
|
|
@@ -320,6 +339,7 @@ export function policyHost(ctx, deps) {
|
|
|
320
339
|
return {
|
|
321
340
|
appliedDecision: (id) => enabled(id) ? applied.get(id)?.id : undefined,
|
|
322
341
|
invalidate: (id) => { epochs.set(id, epoch(id) + 1); mainStatus.delete(id); latest.delete(id); applied.delete(id); },
|
|
342
|
+
childNotice: (rootId) => notices.get(rootId),
|
|
323
343
|
status: (id) => deps.store.get(id), mainStatus: (id) => enabled(id) ? { ...mainStatus.get(id), ...(latest.get(id) ? { decision: latest.get(id) } : {}) } : undefined,
|
|
324
344
|
};
|
|
325
345
|
}
|
package/lib/policy-store.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { type Registry } from './strategies/index.js';
|
|
3
|
+
export declare const FALLBACK_REASONS: readonly ["jev-cancelled", "needs-context", "mode-changed", "interrupted", "no-usable-route", "explicit-unavailable"];
|
|
4
|
+
/** New `unsupported` writes use a closed reason set; `jev-<category>` carries the failure category. */
|
|
5
|
+
export declare const closedReason: (reason: string | undefined) => boolean;
|
|
3
6
|
declare function schemaFor(registry: Registry): z.ZodObject<{
|
|
4
7
|
childId: z.ZodString;
|
|
5
8
|
parentId: z.ZodString;
|
|
@@ -7,8 +10,8 @@ declare function schemaFor(registry: Registry): z.ZodObject<{
|
|
|
7
10
|
status: z.ZodEnum<{
|
|
8
11
|
failed: "failed";
|
|
9
12
|
recommended: "recommended";
|
|
10
|
-
unsupported: "unsupported";
|
|
11
13
|
"needs-context": "needs-context";
|
|
14
|
+
unsupported: "unsupported";
|
|
12
15
|
applied: "applied";
|
|
13
16
|
}>;
|
|
14
17
|
path: z.ZodEnum<{
|
|
@@ -20,12 +23,15 @@ declare function schemaFor(registry: Registry): z.ZodObject<{
|
|
|
20
23
|
[x: string]: string;
|
|
21
24
|
}>>;
|
|
22
25
|
explicit: z.ZodObject<{
|
|
23
|
-
provider: z.
|
|
24
|
-
[x: string]: string;
|
|
25
|
-
}>;
|
|
26
|
+
provider: z.ZodString;
|
|
26
27
|
model: z.ZodOptional<z.ZodString>;
|
|
27
28
|
reasoningEffort: z.ZodOptional<z.ZodString>;
|
|
28
29
|
}, z.core.$strict>;
|
|
30
|
+
parentSelection: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
provider: z.ZodString;
|
|
32
|
+
model: z.ZodOptional<z.ZodString>;
|
|
33
|
+
reasoningEffort: z.ZodOptional<z.ZodString>;
|
|
34
|
+
}, z.core.$strict>>;
|
|
29
35
|
decision: z.ZodOptional<z.ZodObject<{
|
|
30
36
|
id: z.ZodNumber;
|
|
31
37
|
source: z.ZodEnum<{
|
|
@@ -48,9 +54,7 @@ declare function schemaFor(registry: Registry): z.ZodObject<{
|
|
|
48
54
|
capabilityProbabilities: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
49
55
|
}, z.core.$strict>>;
|
|
50
56
|
route: z.ZodOptional<z.ZodObject<{
|
|
51
|
-
provider: z.
|
|
52
|
-
[x: string]: string;
|
|
53
|
-
}>;
|
|
57
|
+
provider: z.ZodString;
|
|
54
58
|
model: z.ZodString;
|
|
55
59
|
reasoningEffort: z.ZodEnum<{
|
|
56
60
|
low: "low";
|
|
@@ -60,9 +64,7 @@ declare function schemaFor(registry: Registry): z.ZodObject<{
|
|
|
60
64
|
}>;
|
|
61
65
|
}, z.core.$strict>>;
|
|
62
66
|
actual: z.ZodOptional<z.ZodObject<{
|
|
63
|
-
provider: z.
|
|
64
|
-
[x: string]: string;
|
|
65
|
-
}>;
|
|
67
|
+
provider: z.ZodString;
|
|
66
68
|
model: z.ZodString;
|
|
67
69
|
reasoningEffort: z.ZodEnum<{
|
|
68
70
|
low: "low";
|
|
@@ -83,6 +85,8 @@ export declare class PolicyStore {
|
|
|
83
85
|
bind(id: string): void;
|
|
84
86
|
wasBound(id: string): boolean;
|
|
85
87
|
get(id: string): ChildDecision | undefined;
|
|
88
|
+
/** Initial binding of a new managed child: the parent snapshot is mandatory, then the marker. */
|
|
89
|
+
create(value: ChildDecision): void;
|
|
86
90
|
set(value: ChildDecision): void;
|
|
87
91
|
}
|
|
88
92
|
export {};
|
package/lib/policy-store.js
CHANGED
|
@@ -4,18 +4,32 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { registry as builtin } from './strategies/index.js';
|
|
6
6
|
const enumOf = (values) => z.enum(values);
|
|
7
|
+
export const FALLBACK_REASONS = ['jev-cancelled', 'needs-context', 'mode-changed', 'interrupted', 'no-usable-route', 'explicit-unavailable'];
|
|
8
|
+
/** New `unsupported` writes use a closed reason set; `jev-<category>` carries the failure category. */
|
|
9
|
+
export const closedReason = (reason) => !!reason && (FALLBACK_REASONS.includes(reason) || /^jev-[a-z-]+$/.test(reason));
|
|
7
10
|
function schemaFor(registry) {
|
|
8
|
-
const
|
|
9
|
-
const route = z.object({ provider
|
|
11
|
+
const provider = z.string().min(1).max(128);
|
|
12
|
+
const route = z.object({ provider, model: z.string().min(1).max(128), reasoningEffort: z.enum(['low', 'medium', 'high', 'xhigh']) }).strict();
|
|
10
13
|
return z.object({
|
|
11
14
|
childId: z.string(), parentId: z.string(), rootId: z.string(),
|
|
12
15
|
status: z.enum(['recommended', 'applied', 'unsupported', 'needs-context', 'failed']),
|
|
13
16
|
path: z.enum(['ordinary', 'teammate', 'fork']),
|
|
14
17
|
family: enumOf(registry.ids()).optional(),
|
|
15
|
-
explicit: z.object({ provider
|
|
18
|
+
explicit: z.object({ provider, model: z.string().optional(), reasoningEffort: z.string().optional() }).strict(),
|
|
19
|
+
parentSelection: z.object({ provider, model: z.string().max(128).optional(), reasoningEffort: z.string().max(32).optional() }).strict().optional(),
|
|
16
20
|
decision: z.object({ id: z.number(), source: z.enum(['main', 'subagent']), tier: z.string(), depth: z.string(), capability: z.string().optional(), effort: z.string(), model: z.string().optional(), provider: z.string().optional(), status: z.string(), ms: z.number(), decisionProvider: z.string(), decisionModel: z.string(), confidence: z.number().min(0).max(1).optional(), capabilityConfidence: z.number().min(0).max(1).optional(), probabilities: z.record(z.string(), z.number().min(0).max(1)).optional(), capabilityProbabilities: z.record(z.string(), z.number().min(0).max(1)).optional() }).strict().optional(),
|
|
17
21
|
route: route.optional(), actual: route.optional(), reason: z.string().max(240).optional(),
|
|
18
|
-
}).strict()
|
|
22
|
+
}).strict().superRefine((r, ctx) => {
|
|
23
|
+
const bad = (message) => ctx.addIssue({ code: 'custom', message });
|
|
24
|
+
if (r.status === 'recommended' && r.actual)
|
|
25
|
+
bad('recommended 不能含 actual');
|
|
26
|
+
if (r.status === 'applied' && (!r.route || !r.actual))
|
|
27
|
+
bad('applied 需要 route 与 actual');
|
|
28
|
+
if (r.status === 'unsupported' && (r.route || r.actual))
|
|
29
|
+
bad('unsupported 不能含 route');
|
|
30
|
+
if (r.status === 'needs-context' && r.route)
|
|
31
|
+
bad('needs-context 不能含 route');
|
|
32
|
+
});
|
|
19
33
|
}
|
|
20
34
|
/** Records written before 0.5.0 carried these keys; reads accept exactly these values, then drop them. */
|
|
21
35
|
const historical = z.object({ version: z.literal(1).optional(), policyVersion: z.string().optional(), mappingVersion: z.string().optional() });
|
|
@@ -62,8 +76,17 @@ export class PolicyStore {
|
|
|
62
76
|
throw new Error('子任务决策无法恢复;暂停该子会话');
|
|
63
77
|
}
|
|
64
78
|
}
|
|
79
|
+
/** Initial binding of a new managed child: the parent snapshot is mandatory, then the marker. */
|
|
80
|
+
create(value) {
|
|
81
|
+
if (!value.parentSelection)
|
|
82
|
+
throw new Error('新建初始记录缺少 parentSelection');
|
|
83
|
+
this.set(value);
|
|
84
|
+
this.bind(value.childId);
|
|
85
|
+
}
|
|
65
86
|
set(value) {
|
|
66
87
|
const checked = this.schema.parse(value);
|
|
88
|
+
if (checked.status === 'unsupported' && !closedReason(checked.reason))
|
|
89
|
+
throw new Error(`未知的未路由原因:${checked.reason ?? ''}`);
|
|
67
90
|
mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
68
91
|
chmodSync(this.directory, 0o700);
|
|
69
92
|
const target = this.path(value.childId), temp = `${target}.${randomUUID()}.tmp`;
|
package/lib/routing.d.ts
CHANGED
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import type { Capability, Depth } from './jev.js';
|
|
2
2
|
import { type AutomaticEffort, type RoutingCatalog } from './settings-contract.js';
|
|
3
3
|
import { type Registry } from './strategies/index.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
type Allowed = readonly {
|
|
5
|
+
provider: string;
|
|
6
|
+
model: string;
|
|
7
|
+
}[];
|
|
8
|
+
/** Whether a route is usable now: live catalog, enabled official allowlist, captured allowlist (if any) and the effort cap. */
|
|
9
|
+
export declare function routeUsable(catalog: RoutingCatalog, provider: string, model: string, effort: string, cap: AutomaticEffort, captured?: Allowed): boolean;
|
|
10
|
+
/** Bound routes are re-checked on every request; any gate failure rejects that request. */
|
|
11
|
+
export declare function validatePolicyRoute(catalog: RoutingCatalog, provider: string, model: string, effort: string, cap: AutomaticEffort, captured?: Allowed): void;
|
|
7
12
|
export declare function policyEffort(model: string, depth: Depth, supported: readonly string[], cap: AutomaticEffort, defaultEffort?: string, registry?: Registry): AutomaticEffort;
|
|
8
13
|
export interface PolicyRoute {
|
|
9
14
|
provider: string;
|
|
10
15
|
model: string;
|
|
11
16
|
reasoningEffort: AutomaticEffort;
|
|
12
17
|
}
|
|
18
|
+
export type RouteFallback = 'no-usable-route' | 'explicit-unavailable';
|
|
19
|
+
export interface ChildRouteInput {
|
|
20
|
+
provider: string;
|
|
21
|
+
parent: {
|
|
22
|
+
model?: string | undefined;
|
|
23
|
+
reasoningEffort?: string | undefined;
|
|
24
|
+
};
|
|
25
|
+
explicit: {
|
|
26
|
+
model?: string | undefined;
|
|
27
|
+
reasoningEffort?: string | undefined;
|
|
28
|
+
};
|
|
29
|
+
capability: Capability;
|
|
30
|
+
depth: Depth;
|
|
31
|
+
catalog: RoutingCatalog;
|
|
32
|
+
cap: AutomaticEffort;
|
|
33
|
+
captured?: Allowed | undefined;
|
|
34
|
+
}
|
|
13
35
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
36
|
+
* Fallback chain: capability slot → keep model with depth effort → parent effort (automatic effort only) → no rewrite.
|
|
37
|
+
* Provider never changes; explicit dimensions are never replaced.
|
|
16
38
|
*/
|
|
17
|
-
export declare function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
export declare function chooseChildRoute(input: ChildRouteInput, registry?: Registry): {
|
|
40
|
+
route: PolicyRoute;
|
|
41
|
+
} | {
|
|
42
|
+
fallback: RouteFallback;
|
|
43
|
+
};
|
|
44
|
+
export {};
|
package/lib/routing.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { AUTO_EFFORTS } from './settings-contract.js';
|
|
2
|
-
import { MODEL_ID_PATTERN } from './settings-validation.js';
|
|
3
2
|
import { registry as builtin } from './strategies/index.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
/** A transport whose model ids carry a family prefix (`<family>/<model>`), e.g. a gateway. */
|
|
9
|
-
function prefixedTransport(provider, registry) {
|
|
10
|
-
return registry.strategies.some(s => (s.transports[provider] ?? '') !== '');
|
|
11
|
-
}
|
|
12
|
-
export function validatePolicyRoute(catalog, provider, model, effort, cap, registry = builtin) {
|
|
3
|
+
const listed = (list, provider, model) => list.some(m => m.provider === provider && m.model === model);
|
|
4
|
+
/** Whether a route is usable now: live catalog, enabled official allowlist, captured allowlist (if any) and the effort cap. */
|
|
5
|
+
export function routeUsable(catalog, provider, model, effort, cap, captured) {
|
|
13
6
|
const entry = catalog.models.find(m => m.provider === provider && m.model === model);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
return !!entry && model.length <= 128 && AUTO_EFFORTS.includes(effort) && AUTO_EFFORTS.indexOf(effort) <= AUTO_EFFORTS.indexOf(cap)
|
|
8
|
+
&& entry.reasoningEfforts.includes(effort) && catalog.childAllowlist.status === 'enabled' && listed(catalog.childAllowlist.allowedModels, provider, model)
|
|
9
|
+
&& (!captured || listed(captured, provider, model));
|
|
10
|
+
}
|
|
11
|
+
/** Bound routes are re-checked on every request; any gate failure rejects that request. */
|
|
12
|
+
export function validatePolicyRoute(catalog, provider, model, effort, cap, captured) {
|
|
13
|
+
if (!routeUsable(catalog, provider, model, effort, cap, captured))
|
|
14
|
+
throw new Error('子任务路由不在当前目录/允许列表或 effort 上限内;拒绝本次请求');
|
|
17
15
|
}
|
|
18
16
|
export function policyEffort(model, depth, supported, cap, defaultEffort, registry = builtin) {
|
|
19
17
|
const start = registry.effortStart(model) ?? defaultEffort;
|
|
@@ -32,20 +30,33 @@ export function policyEffort(model, depth, supported, cap, defaultEffort, regist
|
|
|
32
30
|
return AUTO_EFFORTS.indexOf(chosen) > AUTO_EFFORTS.indexOf(cap) ? allowed.at(-1) : chosen;
|
|
33
31
|
}
|
|
34
32
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* Fallback chain: capability slot → keep model with depth effort → parent effort (automatic effort only) → no rewrite.
|
|
34
|
+
* Provider never changes; explicit dimensions are never replaced.
|
|
37
35
|
*/
|
|
38
|
-
export function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
36
|
+
export function chooseChildRoute(input, registry = builtin) {
|
|
37
|
+
const { provider, parent, explicit, depth, catalog, cap, captured } = input;
|
|
38
|
+
const entry = (model) => catalog.models.find(m => m.provider === provider && m.model === model);
|
|
39
|
+
const depthEffort = (model) => { try {
|
|
40
|
+
const e = entry(model);
|
|
41
|
+
return e ? policyEffort(model, depth, e.reasoningEfforts, cap, e.defaultEffort, registry) : undefined;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
} };
|
|
46
|
+
const take = (model, effort) => model && effort && routeUsable(catalog, provider, model, effort, cap, captured) ? { route: { provider, model, reasoningEffort: effort } } : undefined;
|
|
47
|
+
const family = parent.model ? registry.familyOfMainModel(parent.model)?.id : undefined;
|
|
48
|
+
if (!explicit.model && family && parent.model) {
|
|
49
|
+
const slot = registry.resolveSlot(family, input.capability, parent.model, catalog.models.filter(m => m.provider === provider).map(m => m.model));
|
|
50
|
+
const hit = slot && take(slot, explicit.reasoningEffort ?? depthEffort(slot));
|
|
51
|
+
if (hit)
|
|
52
|
+
return hit;
|
|
53
|
+
}
|
|
54
|
+
const model = explicit.model ?? parent.model;
|
|
55
|
+
const kept = model && take(model, explicit.reasoningEffort ?? depthEffort(model));
|
|
56
|
+
if (kept)
|
|
57
|
+
return kept;
|
|
58
|
+
const inherited = !explicit.reasoningEffort && take(model, parent.reasoningEffort);
|
|
59
|
+
if (inherited)
|
|
60
|
+
return inherited;
|
|
61
|
+
return { fallback: explicit.model || explicit.reasoningEffort ? 'explicit-unavailable' : 'no-usable-route' };
|
|
51
62
|
}
|
package/lib/settings-host.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import { SettingsStore } from './settings.js';
|
|
3
3
|
import type { RoutingCatalog } from './settings-contract.js';
|
|
4
|
-
import { type Registry } from './strategies/index.js';
|
|
5
4
|
/** Only the fixed credential reference and public catalog metadata cross this adapter. */
|
|
6
|
-
export declare function settingsHost(ctx: Context, store: SettingsStore
|
|
7
|
-
catalog: () => Promise<RoutingCatalog>;
|
|
5
|
+
export declare function settingsHost(ctx: Context, store: SettingsStore): {
|
|
6
|
+
catalog: (only?: string) => Promise<RoutingCatalog>;
|
|
8
7
|
read: () => Promise<{
|
|
9
8
|
models: readonly import("./settings-contract.js").RoutingModel[];
|
|
10
9
|
childAllowlist: {
|
package/lib/settings-host.js
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
2
2
|
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
|
|
3
3
|
import { DEFAULT_SETTINGS, SettingsValidationError } from './settings.js';
|
|
4
|
-
import { registry as builtin } from './strategies/index.js';
|
|
5
4
|
import { JevClient } from './jev.js';
|
|
6
5
|
import { DECISION_SERVICES } from './decision-service.js';
|
|
7
6
|
const keyRef = (provider = 'openrouter') => credentialRef(DECISION_SERVICES[provider].credential);
|
|
8
7
|
/** Only the fixed credential reference and public catalog metadata cross this adapter. */
|
|
9
|
-
export function settingsHost(ctx, store
|
|
8
|
+
export function settingsHost(ctx, store) {
|
|
10
9
|
const environment = launchEnvironmentOf(ctx);
|
|
11
10
|
const credentials = () => ctx.get('credentials');
|
|
12
|
-
const
|
|
13
|
-
|
|
11
|
+
const allowlist = () => {
|
|
12
|
+
// The optional provider is owned by dsh-tool-subagent, not this package.
|
|
13
|
+
const service = ctx.get('subagentModelSelection');
|
|
14
|
+
try {
|
|
15
|
+
if (service) {
|
|
16
|
+
const current = service.current();
|
|
17
|
+
return { status: current.enabled ? 'enabled' : 'disabled', allowedModels: current.allowedModels };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch { /* An unavailable policy must never broaden the allowed routes. */ }
|
|
21
|
+
return { status: 'unavailable', allowedModels: [] };
|
|
22
|
+
};
|
|
23
|
+
/** One provider when routing; otherwise only the providers the allowlist names (never every host provider). */
|
|
24
|
+
const catalog = async (only) => {
|
|
25
|
+
const childAllowlist = allowlist();
|
|
26
|
+
const wanted = new Set(only ? [only] : childAllowlist.allowedModels.map(r => r.provider));
|
|
27
|
+
const models = (await Promise.all(ctx.llm.listProviders().filter(p => wanted.has(p.id)).map(async (provider) => {
|
|
14
28
|
try {
|
|
15
29
|
return await Promise.all((await ctx.llm.listModels(provider.id)).map(async (model) => {
|
|
16
30
|
try {
|
|
@@ -26,16 +40,7 @@ export function settingsHost(ctx, store, registry = builtin) {
|
|
|
26
40
|
return [];
|
|
27
41
|
}
|
|
28
42
|
}))).flat();
|
|
29
|
-
|
|
30
|
-
const service = ctx.get('subagentModelSelection');
|
|
31
|
-
try {
|
|
32
|
-
if (service) {
|
|
33
|
-
const current = service.current();
|
|
34
|
-
return { models, childAllowlist: { status: current.enabled ? 'enabled' : 'disabled', allowedModels: current.allowedModels.filter(r => registry.hasProvider(r.provider)) } };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch { /* An unavailable policy must never broaden the allowed routes. */ }
|
|
38
|
-
return { models, childAllowlist: { status: 'unavailable', allowedModels: [] } };
|
|
43
|
+
return { models, childAllowlist };
|
|
39
44
|
};
|
|
40
45
|
const keyStatus = async (provider = store.read().settings.decisionService?.provider ?? 'openrouter') => {
|
|
41
46
|
const ref = keyRef(provider);
|
|
@@ -2,11 +2,6 @@ export declare const claude: {
|
|
|
2
2
|
readonly id: "claude";
|
|
3
3
|
readonly label: "Claude";
|
|
4
4
|
readonly modelPrefixes: readonly ["claude-"];
|
|
5
|
-
readonly transports: {
|
|
6
|
-
readonly claude: "";
|
|
7
|
-
readonly 'dsh-magpie-connect': "claude/";
|
|
8
|
-
readonly 'dsh-magpie-connector': "claude/";
|
|
9
|
-
};
|
|
10
5
|
readonly effortStarts: {
|
|
11
6
|
readonly 'claude-opus-5-5': "medium";
|
|
12
7
|
readonly 'claude-sonnet-5-5': "medium";
|
package/lib/strategies/claude.js
CHANGED
|
@@ -2,7 +2,6 @@ export const claude = {
|
|
|
2
2
|
id: 'claude',
|
|
3
3
|
label: 'Claude',
|
|
4
4
|
modelPrefixes: ['claude-'],
|
|
5
|
-
transports: { claude: '', 'dsh-magpie-connect': 'claude/', 'dsh-magpie-connector': 'claude/' },
|
|
6
5
|
effortStarts: { 'claude-opus-5-5': 'medium', 'claude-sonnet-5-5': 'medium', 'claude-haiku-5-5': 'medium', 'claude-fable-5-1': 'high' },
|
|
7
6
|
childModels: { bounded: 'claude-haiku-5-5', analytical: 'claude-sonnet-5-5', frontier: 'claude-opus-5-5' },
|
|
8
7
|
};
|
|
@@ -3,13 +3,8 @@ export declare const codex: {
|
|
|
3
3
|
readonly id: "codex";
|
|
4
4
|
readonly label: "GPT";
|
|
5
5
|
readonly modelPrefixes: readonly ["gpt-"];
|
|
6
|
-
readonly transports: {
|
|
7
|
-
readonly codex: "";
|
|
8
|
-
readonly 'dsh-magpie-connect': "codex/";
|
|
9
|
-
readonly 'dsh-magpie-connector': "codex/";
|
|
10
|
-
};
|
|
11
6
|
readonly effortStarts: {
|
|
12
|
-
readonly 'gpt-6.1-sol': "
|
|
7
|
+
readonly 'gpt-6.1-sol': "high";
|
|
13
8
|
readonly 'gpt-6-luna': "high";
|
|
14
9
|
readonly 'gpt-6-astra': "low";
|
|
15
10
|
};
|