dsh-auto-model-routing 0.4.2 → 0.5.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/CHANGELOG.md +15 -0
- package/README.md +10 -15
- package/README.zh.md +10 -15
- package/docs/SCREENSHOTS.md +2 -2
- package/docs/auto-routing.gif +0 -0
- package/docs/feedback-evaluation.md +4 -4
- package/docs/host-capability-report.md +2 -0
- package/docs/jev-policy-v2-validation.md +3 -1
- package/docs/magpie-live-acceptance.md +2 -0
- package/docs/policy-v2-development.md +5 -3
- package/docs/policy-v2-real-evaluation-plan.md +2 -0
- package/docs/settings.gif +0 -0
- package/lib/client/control.d.ts +2 -2
- package/lib/client/control.js +10 -20
- package/lib/client/index.d.ts +0 -1
- package/lib/client/index.js +9 -6
- package/lib/client/label.d.ts +7 -10
- package/lib/client/label.js +17 -30
- package/lib/client/settings-section.d.ts +2 -2
- package/lib/client/settings-section.js +7 -69
- package/lib/client/strategy-details.js +3 -4
- package/lib/client.js +165 -481
- package/lib/context.d.ts +1 -3
- package/lib/context.js +3 -4
- package/lib/effort.d.ts +2 -10
- package/lib/effort.js +1 -31
- package/lib/endpoints.d.ts +11 -10
- package/lib/endpoints.js +29 -46
- package/lib/feedback.d.ts +60 -7
- package/lib/feedback.js +15 -5
- package/lib/index.d.ts +4 -0
- package/lib/index.js +17 -86
- package/lib/jev.d.ts +17 -29
- package/lib/jev.js +6 -34
- package/lib/policy-host.d.ts +6 -8
- package/lib/policy-host.js +39 -38
- package/lib/policy-store.d.ts +9 -20
- package/lib/policy-store.js +28 -16
- package/lib/routing.d.ts +20 -0
- package/lib/routing.js +51 -0
- package/lib/rubric.json +15 -6
- package/lib/settings-contract.d.ts +5 -21
- package/lib/settings-contract.js +3 -2
- package/lib/settings-host.d.ts +4 -8
- package/lib/settings-host.js +9 -10
- package/lib/settings-validation.d.ts +1 -7
- package/lib/settings-validation.js +0 -51
- package/lib/settings.d.ts +3 -8
- package/lib/settings.js +18 -177
- package/lib/state.d.ts +2 -4
- package/lib/state.js +2 -7
- package/lib/strategies/claude.d.ts +21 -0
- package/lib/strategies/claude.js +8 -0
- package/lib/strategies/codex.d.ts +21 -0
- package/lib/strategies/codex.js +9 -0
- package/lib/strategies/index.d.ts +22 -0
- package/lib/strategies/index.js +85 -0
- package/lib/strategies/types.d.ts +14 -0
- package/lib/strategies/types.js +1 -0
- package/lib/subagent-tool.d.ts +7 -7
- package/lib/subagent-tool.js +33 -76
- package/lib/support.d.ts +7 -8
- package/lib/support.js +9 -13
- package/lib/usage-report.d.ts +2 -2
- package/lib/usage-report.js +25 -17
- package/package.json +3 -4
- package/skills/auto-model-routing-report/SKILL.md +4 -3
- package/skills/subagent-model-routing/SKILL.md +14 -30
- package/lib/client/route-picker.d.ts +0 -21
- package/lib/client/route-picker.js +0 -75
- package/lib/judgements.d.ts +0 -41
- package/lib/judgements.js +0 -55
- package/lib/policy-v2-rubric.json +0 -21
- package/lib/policy-v2.d.ts +0 -21
- package/lib/policy-v2.js +0 -54
- package/lib/router.d.ts +0 -68
- package/lib/router.js +0 -153
- package/scripts/label-report.mjs +0 -50
package/lib/state.js
CHANGED
|
@@ -2,7 +2,8 @@ import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
|
2
2
|
import { resolve, dirname } from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
//
|
|
5
|
+
// Older files also carried `restore`/`anchor` (ignored, non-strict, dropped on the next write) and
|
|
6
|
+
// `previous` (a retired tier name: still accepted and kept as is, never written anew).
|
|
6
7
|
const stateSchema = z.object({
|
|
7
8
|
mode: z.enum(['AUTO', 'MANUAL']),
|
|
8
9
|
previous: z.enum(['simple', 'standard', 'complex', 'escalated']).optional(),
|
|
@@ -56,12 +57,6 @@ export class RoutingState {
|
|
|
56
57
|
const state = this.read(id);
|
|
57
58
|
return this.save(id, { mode: 'MANUAL', ...(state.previous ? { previous: state.previous } : {}), disabled: { category, at } });
|
|
58
59
|
}
|
|
59
|
-
/** Keep the latest user-turn tier (name only) so a restart still sends it to Jev as context. */
|
|
60
|
-
remember(id, tier) {
|
|
61
|
-
const state = this.read(id);
|
|
62
|
-
if (state.previous !== tier)
|
|
63
|
-
this.save(id, { ...state, previous: tier });
|
|
64
|
-
}
|
|
65
60
|
save(id, state) {
|
|
66
61
|
this.store.set(id, structuredClone(state));
|
|
67
62
|
return structuredClone(state);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const claude: {
|
|
2
|
+
readonly id: "claude";
|
|
3
|
+
readonly label: "Claude";
|
|
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
|
+
readonly effortStarts: {
|
|
11
|
+
readonly 'claude-opus-5-5': "medium";
|
|
12
|
+
readonly 'claude-sonnet-5-5': "medium";
|
|
13
|
+
readonly 'claude-haiku-5-5': "medium";
|
|
14
|
+
readonly 'claude-fable-5-1': "high";
|
|
15
|
+
};
|
|
16
|
+
readonly childModels: {
|
|
17
|
+
readonly bounded: "claude-haiku-5-5";
|
|
18
|
+
readonly analytical: "claude-sonnet-5-5";
|
|
19
|
+
readonly frontier: "claude-opus-5-5";
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const claude = {
|
|
2
|
+
id: 'claude',
|
|
3
|
+
label: 'Claude',
|
|
4
|
+
modelPrefixes: ['claude-'],
|
|
5
|
+
transports: { claude: '', 'dsh-magpie-connect': 'claude/', 'dsh-magpie-connector': 'claude/' },
|
|
6
|
+
effortStarts: { 'claude-opus-5-5': 'medium', 'claude-sonnet-5-5': 'medium', 'claude-haiku-5-5': 'medium', 'claude-fable-5-1': 'high' },
|
|
7
|
+
childModels: { bounded: 'claude-haiku-5-5', analytical: 'claude-sonnet-5-5', frontier: 'claude-opus-5-5' },
|
|
8
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** GPT family; the id stays `codex` (the local transport name). */
|
|
2
|
+
export declare const codex: {
|
|
3
|
+
readonly id: "codex";
|
|
4
|
+
readonly label: "GPT";
|
|
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
|
+
readonly effortStarts: {
|
|
12
|
+
readonly 'gpt-6.1-sol': "medium";
|
|
13
|
+
readonly 'gpt-6-luna': "high";
|
|
14
|
+
readonly 'gpt-6-astra': "low";
|
|
15
|
+
};
|
|
16
|
+
readonly childModels: {
|
|
17
|
+
readonly bounded: "gpt-6-luna";
|
|
18
|
+
readonly analytical: "gpt-6.1-sol";
|
|
19
|
+
readonly frontier: "gpt-6.1-sol";
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** GPT family; the id stays `codex` (the local transport name). */
|
|
2
|
+
export const codex = {
|
|
3
|
+
id: 'codex',
|
|
4
|
+
label: 'GPT',
|
|
5
|
+
modelPrefixes: ['gpt-'],
|
|
6
|
+
transports: { codex: '', 'dsh-magpie-connect': 'codex/', 'dsh-magpie-connector': 'codex/' },
|
|
7
|
+
effortStarts: { 'gpt-6.1-sol': 'medium', 'gpt-6-luna': 'high', 'gpt-6-astra': 'low' },
|
|
8
|
+
childModels: { bounded: 'gpt-6-luna', analytical: 'gpt-6.1-sol', frontier: 'gpt-6.1-sol' },
|
|
9
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Capability, FamilyStrategy, StartEffort } from './types.js';
|
|
2
|
+
export type { Capability, FamilyStrategy, StartEffort } from './types.js';
|
|
3
|
+
export interface Registry {
|
|
4
|
+
readonly strategies: readonly FamilyStrategy[];
|
|
5
|
+
/** Main-model support decision; undefined means AUTO does not manage this model. */
|
|
6
|
+
familyOfMainModel(model: string | undefined): FamilyStrategy | undefined;
|
|
7
|
+
/** Family of a child model on an exact transport provider. */
|
|
8
|
+
familyOfChild(provider: string, model: string): FamilyStrategy | undefined;
|
|
9
|
+
/** Child model id for a family on a transport: transport prefix + capability slot. */
|
|
10
|
+
childModel(familyId: string, provider: string, capability: Capability): string | undefined;
|
|
11
|
+
effortStart(model: string): StartEffort | undefined;
|
|
12
|
+
hasProvider(provider: string): boolean;
|
|
13
|
+
get(familyId: string): FamilyStrategy | undefined;
|
|
14
|
+
providers(): string[];
|
|
15
|
+
ids(): string[];
|
|
16
|
+
labels(): string[];
|
|
17
|
+
}
|
|
18
|
+
/** Validates the whole list up front; any conflict throws and no partial registry exists. */
|
|
19
|
+
export declare function createRegistry(input: readonly FamilyStrategy[]): Registry;
|
|
20
|
+
export declare const BUILTIN_STRATEGIES: readonly FamilyStrategy[];
|
|
21
|
+
/** The registry every module uses unless a test injects strategies through `apply(ctx, { strategies })`. */
|
|
22
|
+
export declare const registry: Registry;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { claude } from './claude.js';
|
|
2
|
+
import { codex } from './codex.js';
|
|
3
|
+
const CAPABILITIES = ['bounded', 'analytical', 'frontier'];
|
|
4
|
+
const START_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
|
|
5
|
+
const bare = (model) => model.slice(model.lastIndexOf('/') + 1);
|
|
6
|
+
const overlaps = (a, b) => a.startsWith(b) || b.startsWith(a);
|
|
7
|
+
/** Validates the whole list up front; any conflict throws and no partial registry exists. */
|
|
8
|
+
export function createRegistry(input) {
|
|
9
|
+
const strategies = input.map(s => structuredClone(s));
|
|
10
|
+
const ids = new Set();
|
|
11
|
+
const starts = new Map();
|
|
12
|
+
const prefixes = [];
|
|
13
|
+
const childPrefixes = new Map();
|
|
14
|
+
for (const s of strategies) {
|
|
15
|
+
if (!s.id)
|
|
16
|
+
throw new Error('策略缺少 id');
|
|
17
|
+
if (ids.has(s.id))
|
|
18
|
+
throw new Error(`策略 id 重复:${s.id}`);
|
|
19
|
+
ids.add(s.id);
|
|
20
|
+
for (const prefix of s.modelPrefixes) {
|
|
21
|
+
const lower = prefix.toLowerCase();
|
|
22
|
+
if (!lower)
|
|
23
|
+
throw new Error(`策略 ${s.id} 的模型前缀为空`);
|
|
24
|
+
const clash = prefixes.find(p => overlaps(p.prefix.toLowerCase(), lower));
|
|
25
|
+
if (clash)
|
|
26
|
+
throw new Error(`模型前缀重叠:${prefix}(${s.id})与 ${clash.prefix}(${clash.id})`);
|
|
27
|
+
prefixes.push({ prefix, id: s.id });
|
|
28
|
+
}
|
|
29
|
+
for (const [model, effort] of Object.entries(s.effortStarts)) {
|
|
30
|
+
if (!START_EFFORTS.includes(effort))
|
|
31
|
+
throw new Error(`策略 ${s.id} 的起点 effort 非法:${model}=${String(effort)}`);
|
|
32
|
+
if (starts.has(model))
|
|
33
|
+
throw new Error(`起点键重复:${model}(${starts.get(model)} 与 ${s.id})`);
|
|
34
|
+
starts.set(model, s.id);
|
|
35
|
+
}
|
|
36
|
+
for (const capability of CAPABILITIES)
|
|
37
|
+
if (typeof s.childModels?.[capability] !== 'string' || !s.childModels[capability])
|
|
38
|
+
throw new Error(`策略 ${s.id} 缺少能力槽:${capability}`);
|
|
39
|
+
for (const [provider, prefix] of Object.entries(s.transports)) {
|
|
40
|
+
const existing = childPrefixes.get(provider) ?? [];
|
|
41
|
+
const clash = existing.find(p => p.prefix === '' || prefix === '' ? true : overlaps(p.prefix, prefix));
|
|
42
|
+
if (clash) {
|
|
43
|
+
if (clash.prefix === '' || prefix === '')
|
|
44
|
+
throw new Error(`provider ${provider} 的空前缀只能属于一个策略(${clash.id} 与 ${s.id} 冲突)`);
|
|
45
|
+
throw new Error(`provider ${provider} 下子模型前缀重叠:${prefix}(${s.id})与 ${clash.prefix}(${clash.id})`);
|
|
46
|
+
}
|
|
47
|
+
childPrefixes.set(provider, [...existing, { prefix, id: s.id }]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const byId = new Map(strategies.map(s => [s.id, s]));
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
strategies,
|
|
53
|
+
familyOfMainModel: (model) => {
|
|
54
|
+
if (typeof model !== 'string' || !model)
|
|
55
|
+
return undefined;
|
|
56
|
+
const name = bare(model).toLowerCase();
|
|
57
|
+
return strategies.find(s => s.modelPrefixes.some(p => name.startsWith(p.toLowerCase())));
|
|
58
|
+
},
|
|
59
|
+
familyOfChild: (provider, model) => {
|
|
60
|
+
const entries = childPrefixes.get(provider) ?? [];
|
|
61
|
+
const hit = entries.find(e => e.prefix !== '' && model.startsWith(e.prefix)) ?? (entries.length === 1 && entries[0].prefix === '' ? entries[0] : undefined);
|
|
62
|
+
return hit ? byId.get(hit.id) : undefined;
|
|
63
|
+
},
|
|
64
|
+
childModel: (familyId, provider, capability) => {
|
|
65
|
+
const s = byId.get(familyId);
|
|
66
|
+
const prefix = s?.transports[provider];
|
|
67
|
+
return s && prefix !== undefined ? `${prefix}${s.childModels[capability]}` : undefined;
|
|
68
|
+
},
|
|
69
|
+
effortStart: (model) => {
|
|
70
|
+
const name = bare(model);
|
|
71
|
+
for (const s of strategies)
|
|
72
|
+
if (Object.hasOwn(s.effortStarts, name))
|
|
73
|
+
return s.effortStarts[name];
|
|
74
|
+
return undefined;
|
|
75
|
+
},
|
|
76
|
+
hasProvider: (provider) => childPrefixes.has(provider),
|
|
77
|
+
get: (familyId) => byId.get(familyId),
|
|
78
|
+
providers: () => [...childPrefixes.keys()],
|
|
79
|
+
ids: () => strategies.map(s => s.id),
|
|
80
|
+
labels: () => strategies.map(s => s.label),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export const BUILTIN_STRATEGIES = [claude, codex];
|
|
84
|
+
/** The registry every module uses unless a test injects strategies through `apply(ctx, { strategies })`. */
|
|
85
|
+
export const registry = createRegistry(BUILTIN_STRATEGIES);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type Capability = 'bounded' | 'analytical' | 'frontier';
|
|
2
|
+
export type StartEffort = 'low' | 'medium' | 'high' | 'xhigh';
|
|
3
|
+
/** Everything AUTO knows about one model family. Pure data: engine rules (depth offsets, caps) stay out. */
|
|
4
|
+
export interface FamilyStrategy {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
/** Main-model support: matched case-insensitively against the id after its last `/`. */
|
|
8
|
+
readonly modelPrefixes: readonly string[];
|
|
9
|
+
/** Transport provider → model-id prefix for this family's child models on that transport. */
|
|
10
|
+
readonly transports: Readonly<Record<string, string>>;
|
|
11
|
+
/** Exact bare model id → recommended starting effort. */
|
|
12
|
+
readonly effortStarts: Readonly<Record<string, StartEffort>>;
|
|
13
|
+
readonly childModels: Readonly<Record<Capability, string>>;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/subagent-tool.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type { Judgement } from './router.js';
|
|
1
|
+
import type { Settings, RoutingCatalog } from './settings-contract.js';
|
|
2
|
+
import type { JevClient } from './jev.js';
|
|
4
3
|
import type { RoutingState } from './state.js';
|
|
4
|
+
import { type Registry } from './strategies/index.js';
|
|
5
5
|
interface SessionLike {
|
|
6
6
|
id: string;
|
|
7
7
|
session: {
|
|
@@ -17,10 +17,10 @@ export interface ToolDeps {
|
|
|
17
17
|
get(id: string): SessionLike | undefined;
|
|
18
18
|
};
|
|
19
19
|
state: RoutingState;
|
|
20
|
-
readSettings
|
|
21
|
-
catalog
|
|
22
|
-
|
|
20
|
+
readSettings: () => Settings;
|
|
21
|
+
catalog: () => RoutingCatalog | Promise<RoutingCatalog>;
|
|
22
|
+
registry?: Registry;
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
24
|
+
/** Preview only: the same Jev judgment AUTO uses, while the root session is AUTO. Writes no state. */
|
|
25
25
|
export declare function routeSubagentTool(jev: JevClient, deps: ToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
26
26
|
export {};
|
package/lib/subagent-tool.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
-
import { CHILD_MODELS, mapEffort, tiers } from './effort.js';
|
|
3
|
-
import { explicitChildRoute, SettingsValidationError } from './settings.js';
|
|
4
|
-
import { DEFAULT_SETTINGS } from './settings-contract.js';
|
|
5
|
-
import { validateChildRoute } from './settings-validation.js';
|
|
6
|
-
import { CATEGORY_HINTS } from './jev.js';
|
|
7
2
|
import { buildState } from './context.js';
|
|
8
|
-
import { choosePolicyRoute,
|
|
9
|
-
import {
|
|
3
|
+
import { choosePolicyRoute, modelFamily } from './routing.js';
|
|
4
|
+
import { registry as builtin } from './strategies/index.js';
|
|
10
5
|
/** The ordinary session a caller belongs to; undefined when any link is no longer live (treated as MANUAL). */
|
|
11
6
|
function rootOf(caller, agents) {
|
|
12
7
|
let current = caller;
|
|
@@ -18,20 +13,21 @@ function rootOf(caller, agents) {
|
|
|
18
13
|
}
|
|
19
14
|
return undefined;
|
|
20
15
|
}
|
|
21
|
-
const MANUAL_TEXT = '主会话为 MANUAL
|
|
22
|
-
/**
|
|
16
|
+
const MANUAL_TEXT = '主会话为 MANUAL:不判断,按官方默认委派;只传用户或 lead 指定的值(例如指定的模型),不传其它 provider/model/reasoning_effort。';
|
|
17
|
+
/** Preview only: the same Jev judgment AUTO uses, while the root session is AUTO. Writes no state. */
|
|
23
18
|
export function routeSubagentTool(jev, deps) {
|
|
19
|
+
const registry = deps.registry ?? builtin;
|
|
24
20
|
return defineTool({
|
|
25
21
|
name: 'route_subagent',
|
|
26
|
-
description: '
|
|
27
|
-
timeoutMs:
|
|
22
|
+
description: 'Optional preview of the AUTO sub-agent route: returns a recommendation only, never an applied route. Automatic routing happens when the official fresh creation tool creates the exact child, before its first request; use that tool directly and do not turn this recommendation into an explicit user constraint. Fork is unsupported. Sends the task text (redacted, up to 8000 characters) to the decision service chosen in settings only while the main session is AUTO; otherwise returns routed=false: delegate with the official defaults. If the user or lead already chose the model or effort, pass it: only the missing dimension is judged. On error: pause delegation and ask the user; never guess or switch models.',
|
|
23
|
+
timeoutMs: 10000,
|
|
28
24
|
parameters: {
|
|
29
25
|
task: { type: 'string', required: true, description: 'The task the sub-agent will do.' },
|
|
30
|
-
provider: { type: 'string', required: true, enum:
|
|
31
|
-
model: { type: 'string', description: 'Sub-agent model already chosen by the user or lead; when given, only reasoning_effort is
|
|
32
|
-
reasoning_effort: { type: 'string', description: '
|
|
33
|
-
family: { type: 'string', enum:
|
|
34
|
-
path: { type: 'string', enum: ['ordinary', 'fork', 'teammate'], description: '
|
|
26
|
+
provider: { type: 'string', required: true, enum: registry.providers(), description: 'Exact transport provider of the sub-agent.' },
|
|
27
|
+
model: { type: 'string', description: 'Sub-agent model already chosen by the user or lead; when given, only reasoning_effort is judged.' },
|
|
28
|
+
reasoning_effort: { type: 'string', description: 'Effort already chosen by the user or lead; preserved and validated, never silently replaced.' },
|
|
29
|
+
family: { type: 'string', enum: registry.ids(), description: 'Model family for an automatic model on a transport shared by several families; required there when no model is supplied.' },
|
|
30
|
+
path: { type: 'string', enum: ['ordinary', 'fork', 'teammate'], description: 'Creation path; fork is unsupported.' },
|
|
35
31
|
},
|
|
36
32
|
output: {
|
|
37
33
|
schema: {
|
|
@@ -40,82 +36,43 @@ export function routeSubagentTool(jev, deps) {
|
|
|
40
36
|
properties: {
|
|
41
37
|
routed: { type: 'boolean' },
|
|
42
38
|
status: { type: 'string' },
|
|
43
|
-
policyVersion: { type: 'string' },
|
|
44
39
|
capability: { type: 'string' },
|
|
45
40
|
depth: { type: 'string' },
|
|
46
41
|
reason: { type: 'string' },
|
|
47
|
-
tier: { type: 'string' },
|
|
48
42
|
provider: { type: 'string' },
|
|
49
43
|
model: { type: 'string' },
|
|
50
44
|
reasoning_effort: { type: 'string' },
|
|
51
45
|
},
|
|
52
46
|
},
|
|
53
|
-
render: (_args, v) => [{ type: 'text', text: v.status
|
|
54
|
-
? `
|
|
55
|
-
:
|
|
56
|
-
|
|
57
|
-
? MANUAL_TEXT
|
|
58
|
-
: `Delegate with provider=${v.provider} model=${v.model} reasoning_effort=${v.reasoning_effort} (tier ${v.tier}); label the description [${v.model}·${v.reasoning_effort}].` }],
|
|
47
|
+
render: (_args, v) => [{ type: 'text', text: v.status === 'recommended'
|
|
48
|
+
? `Recommended only (not applied): ${v.provider}/${v.model}/${v.reasoning_effort}. Use the official creation tool; AUTO judges and binds the exact child before its first request. Do not copy this recommendation as a new explicit user constraint. Query the child routing status for applied evidence.`
|
|
49
|
+
: v.status ? `${v.status}: ${v.reason}. 暂停委派并询问用户;不将推荐冒充实际应用。`
|
|
50
|
+
: MANUAL_TEXT }],
|
|
59
51
|
},
|
|
60
52
|
execute: async (args, exec) => {
|
|
61
|
-
const
|
|
62
|
-
const root = rootOf(caller, deps.agents);
|
|
53
|
+
const root = rootOf(exec.agent, deps.agents);
|
|
63
54
|
// MANUAL (or disabled, or an unreachable root): no judgement and nothing leaves the machine.
|
|
64
55
|
if (!root || deps.state.read(root.id).mode !== 'AUTO')
|
|
65
56
|
return { routed: false, reason: 'manual' };
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const route = choosePolicyRoute(args.provider, result.capability ?? 'bounded', result.depth, await deps.catalog(), deps.readSettings().effortCap, explicit, args.family ?? (args.model ? modelFamily(args.provider, args.model) : undefined));
|
|
77
|
-
return { status: 'recommended', policyVersion, ...(result.capability ? { capability: result.capability } : {}), depth: result.depth, provider: route.provider, model: route.model, reasoning_effort: route.reasoningEffort };
|
|
78
|
-
}
|
|
79
|
-
if (magpieProvider(args.provider))
|
|
80
|
-
throw new Error('Magpie 路由需显式选择 policy-v2;原策略不变');
|
|
81
|
-
const result = await jev.judge(buildState({ text: args.task }), exec.signal);
|
|
82
|
-
if (result.kind === 'cancelled')
|
|
83
|
-
throw new Error('route_subagent 已取消。');
|
|
84
|
-
if (result.kind === 'failed')
|
|
85
|
-
throw new Error(`Jev 判档失败:${CATEGORY_HINTS[result.category]}(${result.category})。请暂停委派并询问用户,不要自行猜测档位或换用其它模型。`);
|
|
86
|
-
const provider = args.provider;
|
|
87
|
-
const defaultModel = CHILD_MODELS[provider][tiers.indexOf(result.tier)];
|
|
88
|
-
let model;
|
|
89
|
-
let effort;
|
|
57
|
+
if (args.path === 'fork')
|
|
58
|
+
return { routed: false, status: 'unsupported', reason: 'fork 的真实历史兼容尚未验证;请改用 fresh' };
|
|
59
|
+
const explicit = { ...(args.model ? { model: args.model } : {}), ...(args.reasoning_effort ? { reasoningEffort: args.reasoning_effort } : {}) };
|
|
60
|
+
const result = explicit.model && explicit.reasoningEffort ? { kind: 'ok', depth: 'baseline', capability: 'bounded' }
|
|
61
|
+
: await jev.judgePolicy(buildState({ text: args.task }), exec.signal, 'child', explicit.model ? 'depth' : explicit.reasoningEffort ? 'capability' : 'both');
|
|
62
|
+
if (result.kind !== 'ok')
|
|
63
|
+
throw new Error('Jev 判断失败或已取消;暂停委派并询问用户');
|
|
64
|
+
if (result.capability === 'insufficient_context')
|
|
65
|
+
return { routed: false, status: 'needs-context', reason: '请补充任务目标与范围' };
|
|
66
|
+
let route;
|
|
90
67
|
try {
|
|
91
|
-
|
|
92
|
-
const catalog = deps.catalog ? await deps.catalog() : {
|
|
93
|
-
models: deps.readSettings ? [] : [{ provider, model: defaultModel, name: defaultModel, reasoningEfforts: ['low', 'medium', 'high', 'xhigh'] }],
|
|
94
|
-
childAllowlist: { status: 'enabled', allowedModels: deps.readSettings ? [] : [{ provider, model: defaultModel }] },
|
|
95
|
-
};
|
|
96
|
-
if (exec.signal.aborted)
|
|
97
|
-
throw new Error('cancelled');
|
|
98
|
-
const settings = deps.readSettings?.() ?? DEFAULT_SETTINGS;
|
|
99
|
-
// A caller-chosen model skips the tier→model table entirely (childRoutes included); only effort is routed.
|
|
100
|
-
const explicit = args.model ? undefined : explicitChildRoute(settings, provider, result.tier, catalog);
|
|
101
|
-
model = args.model || (explicit?.model ?? defaultModel);
|
|
102
|
-
const supported = catalog.models.find(entry => entry.provider === provider && entry.model === model)?.reasoningEfforts ?? [];
|
|
103
|
-
const mapped = explicit?.reasoningEffort ?? mapEffort(model, result.tier, supported, settings.effortCap);
|
|
104
|
-
if (!mapped || validateChildRoute(catalog, provider, { model, reasoningEffort: mapped }, settings.effortCap).length)
|
|
105
|
-
throw new SettingsValidationError([]);
|
|
106
|
-
effort = mapped;
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
if (exec.signal.aborted)
|
|
110
|
-
throw new Error('route_subagent 已取消。');
|
|
111
|
-
throw new Error('自动模型路由设置无效。请暂停委派并询问用户,修复设置后重试,不要自行换路由。');
|
|
68
|
+
route = choosePolicyRoute(args.provider, result.capability ?? 'bounded', result.depth, await deps.catalog(), deps.readSettings().effortCap, explicit, args.family ?? (args.model ? modelFamily(args.provider, args.model, registry) : undefined), registry);
|
|
112
69
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
70
|
+
catch (error) {
|
|
71
|
+
// No usable route: never substitute another model or effort.
|
|
72
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
73
|
+
throw new Error(reason.includes('暂停委派并询问用户') ? reason : `${reason};暂停委派并询问用户`);
|
|
117
74
|
}
|
|
118
|
-
return {
|
|
75
|
+
return { status: 'recommended', ...(result.capability ? { capability: result.capability } : {}), depth: result.depth, provider: route.provider, model: route.model, reasoning_effort: route.reasoningEffort };
|
|
119
76
|
},
|
|
120
77
|
});
|
|
121
78
|
}
|
package/lib/support.d.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
3
|
+
import { type Registry } from './strategies/index.js';
|
|
3
4
|
/** One main-session model selection as the AUTO support gate needs it. */
|
|
4
5
|
export interface MainSelection {
|
|
5
6
|
provider: string;
|
|
6
7
|
model: string;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
|
-
* AUTO only manages
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* GLM, Grok, Gemini, Kimi, Qwen, ... — is explicitly out of scope and locks the
|
|
14
|
-
* session to MANUAL instead of being guessed at.
|
|
10
|
+
* AUTO only manages models whose family has a registered strategy. The transport
|
|
11
|
+
* prefix (e.g. a gateway's `<family>/`) is not part of the family name, so only the
|
|
12
|
+
* part after the last `/` is matched. Other families lock the session to MANUAL
|
|
13
|
+
* instead of being guessed at.
|
|
15
14
|
*/
|
|
16
|
-
export declare function autoSupportedModel(model: string | undefined): boolean;
|
|
15
|
+
export declare function autoSupportedModel(model: string | undefined, registry?: Registry): boolean;
|
|
17
16
|
/** An unknown selection must never lock a session: only a proven mismatch does. */
|
|
18
|
-
export declare function mainSupported(selection: MainSelection | undefined): boolean;
|
|
17
|
+
export declare function mainSupported(selection: MainSelection | undefined, registry?: Registry): boolean;
|
|
19
18
|
/**
|
|
20
19
|
* The main session's current model selection, most-authoritative signal first:
|
|
21
20
|
* the official pending pick (`model/selection` projection), then the last actual
|
package/lib/support.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
|
+
import { registry as builtin } from './strategies/index.js';
|
|
1
2
|
/**
|
|
2
|
-
* AUTO only manages
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* GLM, Grok, Gemini, Kimi, Qwen, ... — is explicitly out of scope and locks the
|
|
7
|
-
* session to MANUAL instead of being guessed at.
|
|
3
|
+
* AUTO only manages models whose family has a registered strategy. The transport
|
|
4
|
+
* prefix (e.g. a gateway's `<family>/`) is not part of the family name, so only the
|
|
5
|
+
* part after the last `/` is matched. Other families lock the session to MANUAL
|
|
6
|
+
* instead of being guessed at.
|
|
8
7
|
*/
|
|
9
|
-
export function autoSupportedModel(model) {
|
|
10
|
-
|
|
11
|
-
return false;
|
|
12
|
-
const bare = model.includes('/') ? model.slice(model.lastIndexOf('/') + 1) : model;
|
|
13
|
-
return /^(?:claude|gpt)-/i.test(bare);
|
|
8
|
+
export function autoSupportedModel(model, registry = builtin) {
|
|
9
|
+
return registry.familyOfMainModel(model) !== undefined;
|
|
14
10
|
}
|
|
15
11
|
/** An unknown selection must never lock a session: only a proven mismatch does. */
|
|
16
|
-
export function mainSupported(selection) {
|
|
17
|
-
return selection === undefined || autoSupportedModel(selection.model);
|
|
12
|
+
export function mainSupported(selection, registry = builtin) {
|
|
13
|
+
return selection === undefined || autoSupportedModel(selection.model, registry);
|
|
18
14
|
}
|
|
19
15
|
/**
|
|
20
16
|
* The main session's current model selection, most-authoritative signal first:
|
package/lib/usage-report.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ type Failure = 'decode-failed' | 'tail-incomplete' | 'parse-failed' | 'missing-l
|
|
|
2
2
|
export declare function buildReport(home: string, options?: {
|
|
3
3
|
since?: string;
|
|
4
4
|
}): {
|
|
5
|
-
schema: number;
|
|
6
5
|
since: string | null;
|
|
7
6
|
failures: Record<Failure, number>;
|
|
8
7
|
excluded: {
|
|
@@ -16,7 +15,8 @@ export declare function buildReport(home: string, options?: {
|
|
|
16
15
|
autoSessionsWithoutRecord: number;
|
|
17
16
|
};
|
|
18
17
|
groups: {
|
|
19
|
-
|
|
18
|
+
group: string;
|
|
19
|
+
historical: boolean;
|
|
20
20
|
subgroups: {
|
|
21
21
|
model: string;
|
|
22
22
|
depth: string;
|
package/lib/usage-report.js
CHANGED
|
@@ -184,6 +184,14 @@ function median(values) {
|
|
|
184
184
|
const s = [...values].sort((a, b) => a - b), mid = Math.floor(s.length / 2);
|
|
185
185
|
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
186
186
|
}
|
|
187
|
+
/** Group key: the strategy family; for snapshots written before 0.5.0 the original fields, verbatim (historical); else `unknown`. */
|
|
188
|
+
function groupOf(snapshot) {
|
|
189
|
+
if (snapshot?.strategy)
|
|
190
|
+
return { group: snapshot.strategy, historical: false };
|
|
191
|
+
if (snapshot?.policyVersion !== undefined || snapshot?.mappingVersion !== undefined)
|
|
192
|
+
return { group: `${snapshot.policyVersion ?? ''}/${snapshot.mappingVersion ?? ''}`, historical: true };
|
|
193
|
+
return { group: 'unknown', historical: false };
|
|
194
|
+
}
|
|
187
195
|
export function buildReport(home, options = {}) {
|
|
188
196
|
const failures = { 'decode-failed': 0, 'tail-incomplete': 0, 'parse-failed': 0, 'missing-log': 0 };
|
|
189
197
|
const state = join(home, 'state/auto-model-routing');
|
|
@@ -193,13 +201,14 @@ export function buildReport(home, options = {}) {
|
|
|
193
201
|
lines(feedbackFile, failures); // counts unparsable lines; FeedbackBook skips them silently
|
|
194
202
|
const book = new FeedbackBook(feedbackFile);
|
|
195
203
|
const bindings = book.entries();
|
|
196
|
-
|
|
204
|
+
// Historical four-tier ledger: read only to locate each session's first AUTO record.
|
|
205
|
+
const tierFirst = new Map();
|
|
197
206
|
for (const line of lines(join(state, 'judgements.jsonl'), failures)) {
|
|
198
207
|
if (typeof line?.sessionId !== 'string' || !Number.isInteger(line.turn) || typeof line.tier !== 'string')
|
|
199
208
|
continue;
|
|
200
|
-
|
|
209
|
+
tierFirst.set(line.sessionId, Math.min(tierFirst.get(line.sessionId) ?? Infinity, line.turn));
|
|
201
210
|
}
|
|
202
|
-
const ledgerSessions = [...new Set([...bindings.map(b => b.sessionId), ...
|
|
211
|
+
const ledgerSessions = [...new Set([...bindings.map(b => b.sessionId), ...tierFirst.keys()])].sort();
|
|
203
212
|
const paths = logPaths(home);
|
|
204
213
|
const excluded = { child: 0, seeded: 0 };
|
|
205
214
|
const decisions = new Map();
|
|
@@ -233,18 +242,18 @@ export function buildReport(home, options = {}) {
|
|
|
233
242
|
next = log.userStarts.get(turn);
|
|
234
243
|
}
|
|
235
244
|
const linked = new Set();
|
|
236
|
-
let
|
|
245
|
+
let bindingFirst;
|
|
237
246
|
for (const call of log.calls) {
|
|
238
247
|
const b = book.lookup(session, call.id);
|
|
239
248
|
if (!b)
|
|
240
249
|
continue;
|
|
241
250
|
linked.add(call.id);
|
|
242
251
|
if (call.userTurn !== undefined)
|
|
243
|
-
|
|
252
|
+
bindingFirst = Math.min(bindingFirst ?? Infinity, call.userTurn);
|
|
244
253
|
const key = JSON.stringify([session, b.decisionId]);
|
|
245
254
|
let d = decisions.get(key);
|
|
246
255
|
if (!d) {
|
|
247
|
-
d = { session, id: b.decisionId,
|
|
256
|
+
d = { session, id: b.decisionId, ...groupOf(b.snapshot), model: b.model, depth: b.depth, effort: b.effort, calls: [], feedback: new Set() };
|
|
248
257
|
decisions.set(key, d);
|
|
249
258
|
}
|
|
250
259
|
d.calls.push(call);
|
|
@@ -254,11 +263,11 @@ export function buildReport(home, options = {}) {
|
|
|
254
263
|
d.feedback.add(b.feedback);
|
|
255
264
|
}
|
|
256
265
|
unlinkedBindings += bindings.filter(b => b.sessionId === session && !linked.has(b.messageId)).length;
|
|
257
|
-
// Coverage: user turns from the first
|
|
258
|
-
if (
|
|
266
|
+
// Coverage: user turns from the first allocation binding on (any snapshot format), with calls in range but none linked.
|
|
267
|
+
if (bindingFirst !== undefined) {
|
|
259
268
|
const turns = new Map();
|
|
260
269
|
for (const call of log.calls) {
|
|
261
|
-
if (call.userTurn === undefined || call.userTurn <
|
|
270
|
+
if (call.userTurn === undefined || call.userTurn < bindingFirst || !after(call.time))
|
|
262
271
|
continue;
|
|
263
272
|
turns.set(call.userTurn, (turns.get(call.userTurn) ?? false) || linked.has(call.id));
|
|
264
273
|
}
|
|
@@ -266,7 +275,7 @@ export function buildReport(home, options = {}) {
|
|
|
266
275
|
turnsWithoutRecord += [...turns.values()].filter(v => !v).length;
|
|
267
276
|
}
|
|
268
277
|
// Reference: calls before the earliest record in either ledger (effort only, never tokens).
|
|
269
|
-
const first = Math.min(
|
|
278
|
+
const first = Math.min(tierFirst.get(session) ?? Infinity, bindingFirst ?? Infinity);
|
|
270
279
|
for (const call of log.calls) {
|
|
271
280
|
if (call.userTurn === undefined || call.userTurn >= first || !after(call.time) || !call.model)
|
|
272
281
|
continue;
|
|
@@ -279,15 +288,15 @@ export function buildReport(home, options = {}) {
|
|
|
279
288
|
const kept = [...decisions.values()].filter(d => after(Math.min(...d.calls.map(c => c.time))));
|
|
280
289
|
for (const d of kept)
|
|
281
290
|
bump((autoEffort[d.model] ??= {}), d.effort, d.calls.length);
|
|
282
|
-
const
|
|
291
|
+
const byGroup = new Map();
|
|
283
292
|
for (const d of kept) {
|
|
284
|
-
const
|
|
285
|
-
|
|
293
|
+
const entry = byGroup.get(d.group) ?? { historical: d.historical, subs: new Map() };
|
|
294
|
+
byGroup.set(d.group, entry);
|
|
286
295
|
const key = JSON.stringify([d.model, d.depth]);
|
|
287
|
-
|
|
296
|
+
entry.subs.set(key, [...(entry.subs.get(key) ?? []), d]);
|
|
288
297
|
}
|
|
289
|
-
const groups = [...
|
|
290
|
-
|
|
298
|
+
const groups = [...byGroup].sort(([a], [b]) => a.localeCompare(b)).map(([group, { historical, subs }]) => ({
|
|
299
|
+
group, historical,
|
|
291
300
|
subgroups: [...subs].sort(([a], [b]) => a.localeCompare(b)).map(([key, list]) => {
|
|
292
301
|
const [model, depth] = JSON.parse(key);
|
|
293
302
|
const calls = list.flatMap(d => d.calls);
|
|
@@ -359,7 +368,6 @@ export function buildReport(home, options = {}) {
|
|
|
359
368
|
autoSessionsWithoutRecord++;
|
|
360
369
|
}
|
|
361
370
|
return {
|
|
362
|
-
schema: 1,
|
|
363
371
|
since: since?.label ?? null,
|
|
364
372
|
failures, excluded,
|
|
365
373
|
coverage: { userTurns, turnsWithoutRecord, unlinkedBindings, autoSessionsWithoutRecord },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-auto-model-routing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "DeepSeek Harness plugin: independent Jev capability and effort decisions, enforced fresh-child routes, decision visibility and allocation feedback",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "prgrmrwy",
|
|
@@ -41,8 +41,7 @@
|
|
|
41
41
|
"CHANGELOG.md",
|
|
42
42
|
"skills",
|
|
43
43
|
"docs",
|
|
44
|
-
"screenshots.json"
|
|
45
|
-
"scripts/label-report.mjs"
|
|
44
|
+
"screenshots.json"
|
|
46
45
|
],
|
|
47
46
|
"dsh": {
|
|
48
47
|
"bundle": {
|
|
@@ -59,7 +58,7 @@
|
|
|
59
58
|
}
|
|
60
59
|
},
|
|
61
60
|
"scripts": {
|
|
62
|
-
"build": "tsc -p tsconfig.json && tsdown",
|
|
61
|
+
"build": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.json && tsdown",
|
|
63
62
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
64
63
|
"test": "vitest run",
|
|
65
64
|
"prepare": "npm run build",
|