dsh-auto-model-routing 0.4.1 → 0.5.0
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 +19 -0
- package/README.md +15 -10
- package/README.zh.md +15 -9
- package/docs/feedback-evaluation.md +20 -3
- 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/lib/client/control.d.ts +2 -2
- package/lib/client/control.js +10 -20
- package/lib/client/index.d.ts +9 -2
- package/lib/client/index.js +97 -12
- package/lib/client/label.d.ts +8 -11
- package/lib/client/label.js +51 -29
- 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 +316 -489
- 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 -9
- package/lib/endpoints.js +44 -42
- package/lib/feedback.d.ts +77 -6
- package/lib/feedback.js +34 -3
- package/lib/index.d.ts +4 -0
- package/lib/index.js +54 -89
- package/lib/jev.d.ts +17 -29
- package/lib/jev.js +6 -34
- package/lib/policy-host.d.ts +13 -8
- package/lib/policy-host.js +98 -47
- package/lib/policy-store.d.ts +9 -17
- package/lib/policy-store.js +28 -15
- 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 -9
- 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 +24 -0
- package/lib/support.js +36 -0
- package/lib/usage-report.d.ts +21 -2
- package/lib/usage-report.js +86 -22
- package/package.json +19 -7
- package/skills/auto-model-routing-report/SKILL.md +9 -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 -20
- package/lib/policy-v2.js +0 -53
- package/lib/router.d.ts +0 -68
- package/lib/router.js +0 -153
- package/scripts/label-report.mjs +0 -50
package/lib/settings-host.d.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
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';
|
|
4
5
|
/** Only the fixed credential reference and public catalog metadata cross this adapter. */
|
|
5
|
-
export declare function settingsHost(ctx: Context, store: SettingsStore): {
|
|
6
|
-
catalog: (
|
|
6
|
+
export declare function settingsHost(ctx: Context, store: SettingsStore, registry?: Registry): {
|
|
7
|
+
catalog: () => Promise<RoutingCatalog>;
|
|
7
8
|
read: () => Promise<{
|
|
8
|
-
criteria: {
|
|
9
|
-
simple: string;
|
|
10
|
-
standard: string;
|
|
11
|
-
complex: string;
|
|
12
|
-
escalated: string;
|
|
13
|
-
};
|
|
14
9
|
models: readonly import("./settings-contract.js").RoutingModel[];
|
|
15
10
|
childAllowlist: {
|
|
16
11
|
status: "enabled" | "disabled" | "unavailable";
|
|
@@ -23,6 +18,7 @@ export declare function settingsHost(ctx: Context, store: SettingsStore): {
|
|
|
23
18
|
defaults: Readonly<import("./settings-contract.js").Settings>;
|
|
24
19
|
configStatus: "ok" | "missing" | "invalid";
|
|
25
20
|
issues: import("./settings-contract.js").SettingsIssue[];
|
|
21
|
+
deprecated: string[];
|
|
26
22
|
key: {
|
|
27
23
|
available: boolean;
|
|
28
24
|
configured: boolean;
|
package/lib/settings-host.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
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
|
|
4
|
+
import { registry as builtin } from './strategies/index.js';
|
|
5
5
|
import { JevClient } from './jev.js';
|
|
6
6
|
import { DECISION_SERVICES } from './decision-service.js';
|
|
7
7
|
const keyRef = (provider = 'openrouter') => credentialRef(DECISION_SERVICES[provider].credential);
|
|
8
8
|
/** Only the fixed credential reference and public catalog metadata cross this adapter. */
|
|
9
|
-
export function settingsHost(ctx, store) {
|
|
9
|
+
export function settingsHost(ctx, store, registry = builtin) {
|
|
10
10
|
const environment = launchEnvironmentOf(ctx);
|
|
11
11
|
const credentials = () => ctx.get('credentials');
|
|
12
|
-
const catalog = async (
|
|
13
|
-
const models = (await Promise.all(ctx.llm.listProviders().filter(p =>
|
|
12
|
+
const catalog = async () => {
|
|
13
|
+
const models = (await Promise.all(ctx.llm.listProviders().filter(p => registry.hasProvider(p.id)).map(async (provider) => {
|
|
14
14
|
try {
|
|
15
15
|
return await Promise.all((await ctx.llm.listModels(provider.id)).map(async (model) => {
|
|
16
16
|
try {
|
|
@@ -31,7 +31,7 @@ export function settingsHost(ctx, store) {
|
|
|
31
31
|
try {
|
|
32
32
|
if (service) {
|
|
33
33
|
const current = service.current();
|
|
34
|
-
return { models, childAllowlist: { status: current.enabled ? 'enabled' : 'disabled', allowedModels: current.allowedModels.filter(r =>
|
|
34
|
+
return { models, childAllowlist: { status: current.enabled ? 'enabled' : 'disabled', allowedModels: current.allowedModels.filter(r => registry.hasProvider(r.provider)) } };
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
catch { /* An unavailable policy must never broaden the allowed routes. */ }
|
|
@@ -52,20 +52,20 @@ export function settingsHost(ctx, store) {
|
|
|
52
52
|
};
|
|
53
53
|
const read = async () => {
|
|
54
54
|
const current = await catalog();
|
|
55
|
-
const result = store.read(
|
|
56
|
-
return { settings: result.settings, defaults: DEFAULT_SETTINGS, configStatus: result.status, issues: result.issues, key: await keyStatus(), ...current
|
|
55
|
+
const result = store.read();
|
|
56
|
+
return { settings: result.settings, defaults: DEFAULT_SETTINGS, configStatus: result.status, issues: result.issues, deprecated: result.deprecated, key: await keyStatus(), ...current };
|
|
57
57
|
};
|
|
58
58
|
const endpoint = async (operation, body) => {
|
|
59
59
|
if (operation === 'settings')
|
|
60
60
|
return Response.json(await read());
|
|
61
61
|
if (operation === 'testDecision') {
|
|
62
|
-
const client = new JevClient({ get: async (name) => credentials() ? credentials().resolve(credentialRef(name)) : environment.get(name) }, undefined,
|
|
62
|
+
const client = new JevClient({ get: async (name) => credentials() ? credentials().resolve(credentialRef(name)) : environment.get(name) }, undefined, 8000, () => store.read().settings.decisionService);
|
|
63
63
|
const result = await client.judgePolicy('Public connection test: sort the numbers [3,1,2] in ascending order.', new AbortController().signal, 'child');
|
|
64
64
|
return Response.json(result);
|
|
65
65
|
}
|
|
66
66
|
if (operation === 'saveSettings') {
|
|
67
67
|
try {
|
|
68
|
-
store.save(body
|
|
68
|
+
store.save(body);
|
|
69
69
|
return Response.json(await read());
|
|
70
70
|
}
|
|
71
71
|
catch (error) {
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { SettingsIssue } from './settings-contract.js';
|
|
2
2
|
export declare const MODEL_ID_PATTERN: RegExp;
|
|
3
3
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
4
4
|
export declare function issue(path: string, code: string, message: string): SettingsIssue;
|
|
5
|
-
export declare function supportedAutomaticEfforts(model: RoutingModel, cap: AutomaticEffort): AutomaticEffort[];
|
|
6
|
-
export declare function selectableModels(catalog: RoutingCatalog, provider: string, kind: 'main' | 'child'): RoutingModel[];
|
|
7
|
-
export declare function supportedFamilyEfforts(catalog: RoutingCatalog, provider: string, cap: AutomaticEffort): AutomaticEffort[];
|
|
8
|
-
export declare function validateFamilyMainEffort(catalog: RoutingCatalog, provider: string, effort: unknown, cap: AutomaticEffort, path?: string): SettingsIssue[];
|
|
9
|
-
export declare function validateMainRoute(catalog: RoutingCatalog, provider: string, model: string, effort: unknown, cap: AutomaticEffort, path?: string): SettingsIssue[];
|
|
10
|
-
export declare function validateChildRoute(catalog: RoutingCatalog, provider: string, route: unknown, cap: AutomaticEffort, path?: string): SettingsIssue[];
|
|
@@ -1,56 +1,5 @@
|
|
|
1
|
-
import { AUTO_EFFORTS } from './settings-contract.js';
|
|
2
1
|
export const MODEL_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}(?![\s\S])/;
|
|
3
2
|
export function isRecord(value) {
|
|
4
3
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
4
|
}
|
|
6
5
|
export function issue(path, code, message) { return { path, code, message }; }
|
|
7
|
-
export function supportedAutomaticEfforts(model, cap) {
|
|
8
|
-
return AUTO_EFFORTS.filter((effort, index) => index <= AUTO_EFFORTS.indexOf(cap) && model.reasoningEfforts.includes(effort));
|
|
9
|
-
}
|
|
10
|
-
export function selectableModels(catalog, provider, kind) {
|
|
11
|
-
return catalog.models.filter(model => model.provider === provider && (kind === 'main' || (catalog.childAllowlist.status === 'enabled' && catalog.childAllowlist.allowedModels.some(route => route.provider === provider && route.model === model.model))));
|
|
12
|
-
}
|
|
13
|
-
export function supportedFamilyEfforts(catalog, provider, cap) {
|
|
14
|
-
const models = selectableModels(catalog, provider, 'main');
|
|
15
|
-
return models.length ? AUTO_EFFORTS.filter((effort, index) => index <= AUTO_EFFORTS.indexOf(cap) && models.every(model => model.reasoningEfforts.includes(effort))) : [];
|
|
16
|
-
}
|
|
17
|
-
export function validateFamilyMainEffort(catalog, provider, effort, cap, path = '') {
|
|
18
|
-
if (!['claude', 'codex'].includes(provider))
|
|
19
|
-
return [issue(path, 'invalid-route', 'Invalid provider')];
|
|
20
|
-
if (!AUTO_EFFORTS.includes(effort))
|
|
21
|
-
return [issue(path, 'invalid-effort', 'Choose an automatic effort from low through xhigh')];
|
|
22
|
-
if (AUTO_EFFORTS.indexOf(effort) > AUTO_EFFORTS.indexOf(cap))
|
|
23
|
-
return [issue(path, 'effort-cap', '超过推理强度上限')];
|
|
24
|
-
if (!selectableModels(catalog, provider, 'main').length)
|
|
25
|
-
return [issue(path, 'model-unavailable', '该体系没有当前可用模型')];
|
|
26
|
-
if (!supportedFamilyEfforts(catalog, provider, cap).includes(effort))
|
|
27
|
-
return [issue(path, 'unsupported-effort', '此推理等级并非该体系所有模型共同支持')];
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
30
|
-
export function validateMainRoute(catalog, provider, model, effort, cap, path = '') {
|
|
31
|
-
if (!['claude', 'codex'].includes(provider) || !MODEL_ID_PATTERN.test(model))
|
|
32
|
-
return [issue(path, 'invalid-route', 'Invalid provider or model id')];
|
|
33
|
-
if (!AUTO_EFFORTS.includes(effort))
|
|
34
|
-
return [issue(path, 'invalid-effort', 'Choose an automatic effort from low through xhigh')];
|
|
35
|
-
if (AUTO_EFFORTS.indexOf(effort) > AUTO_EFFORTS.indexOf(cap))
|
|
36
|
-
return [issue(path, 'effort-cap', '超过推理强度上限')];
|
|
37
|
-
const entry = catalog.models.find(entry => entry.provider === provider && entry.model === model);
|
|
38
|
-
if (!entry)
|
|
39
|
-
return [issue(path, 'model-unavailable', '模型不在当前可用目录中')];
|
|
40
|
-
if (!entry.reasoningEfforts.includes(effort))
|
|
41
|
-
return [issue(path, 'unsupported-effort', '模型不支持此推理等级')];
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
export function validateChildRoute(catalog, provider, route, cap, path = '') {
|
|
45
|
-
if (!isRecord(route) || typeof route.model !== 'string' || typeof route.reasoningEffort !== 'string' || Object.keys(route).some(key => !['model', 'reasoningEffort'].includes(key))) {
|
|
46
|
-
return [issue(path, 'invalid-route', '子会话推荐必须包含完整的模型和推理等级')];
|
|
47
|
-
}
|
|
48
|
-
const errors = validateMainRoute(catalog, provider, route.model, route.reasoningEffort, cap, path);
|
|
49
|
-
if (errors.length)
|
|
50
|
-
return errors;
|
|
51
|
-
if (catalog.childAllowlist.status !== 'enabled')
|
|
52
|
-
return [issue(path, 'allowlist-unavailable', '无法读取已启用的子智能体允许列表')];
|
|
53
|
-
if (!catalog.childAllowlist.allowedModels.some(entry => entry.provider === provider && entry.model === route.model))
|
|
54
|
-
return [issue(path, 'route-not-allowed', '模型不在子智能体允许列表中')];
|
|
55
|
-
return [];
|
|
56
|
-
}
|
package/lib/settings.d.ts
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type AutomaticEffort, type RoutingCatalog, type Settings, type SettingsIssue, type SettingsRead } from './settings-contract.js';
|
|
1
|
+
import { type SettingsIssue, type SettingsRead } from './settings-contract.js';
|
|
3
2
|
export { DEFAULT_SETTINGS } from './settings-contract.js';
|
|
4
3
|
export type { Settings, SettingsPatch, SettingsIssue, SettingsRead, RoutingCatalog } from './settings-contract.js';
|
|
5
4
|
export declare class SettingsValidationError extends Error {
|
|
6
5
|
readonly issues: SettingsIssue[];
|
|
7
6
|
constructor(issues: SettingsIssue[]);
|
|
8
7
|
}
|
|
9
|
-
export declare function validateSettingsRoutes(settings: Settings, catalog: RoutingCatalog): SettingsIssue[];
|
|
10
|
-
/** Returns undefined only for a genuinely absent cell; malformed containers/cells throw. */
|
|
11
|
-
export declare function explicitMainEffort(settings: Settings, provider: string, model: string, tier: Tier, catalog: RoutingCatalog): AutomaticEffort | undefined;
|
|
12
|
-
export declare function explicitChildRoute(settings: Settings, provider: string, tier: Tier, catalog: RoutingCatalog): import('./settings-contract.js').ChildRoute | undefined;
|
|
13
8
|
export declare class SettingsStore {
|
|
14
9
|
readonly path: string;
|
|
15
10
|
constructor(path: string);
|
|
16
|
-
read(
|
|
17
|
-
save(patch: unknown
|
|
11
|
+
read(): SettingsRead;
|
|
12
|
+
save(patch: unknown): SettingsRead;
|
|
18
13
|
}
|
package/lib/settings.js
CHANGED
|
@@ -3,9 +3,8 @@ import { dirname } from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { resolveDecisionService } from './decision-service.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { isRecord, issue, MODEL_ID_PATTERN, validateChildRoute, validateFamilyMainEffort, validateMainRoute } from './settings-validation.js';
|
|
6
|
+
import { AUTO_EFFORTS, DEFAULT_SETTINGS, DEPRECATED_KEYS } from './settings-contract.js';
|
|
7
|
+
import { isRecord, issue } from './settings-validation.js';
|
|
9
8
|
export { DEFAULT_SETTINGS } from './settings-contract.js';
|
|
10
9
|
const ordinary = {
|
|
11
10
|
decisionService: z.object({ provider: z.enum(['openrouter', 'typesafe', 'concentrate', 'custom']), model: z.string().regex(/^[A-Za-z0-9._:/~-]{1,128}$/).optional(), endpoint: z.string().max(2048).optional() }).strict().refine(value => { try {
|
|
@@ -15,24 +14,10 @@ const ordinary = {
|
|
|
15
14
|
catch {
|
|
16
15
|
return false;
|
|
17
16
|
} }, 'Invalid decision service'),
|
|
18
|
-
policy: z.enum(['legacy', 'policy-v2']),
|
|
19
17
|
decisionToast: z.boolean(), defaultMode: z.enum(['MANUAL', 'AUTO']),
|
|
20
|
-
effortCap: z.enum(AUTO_EFFORTS), jevTimeoutMs: z.number().int().min(1000).max(
|
|
21
|
-
};
|
|
22
|
-
const provider = z.enum(['claude', 'codex']);
|
|
23
|
-
const tier = z.enum(['simple', 'standard', 'complex', 'escalated']);
|
|
24
|
-
const model = z.string().regex(MODEL_ID_PATTERN);
|
|
25
|
-
const effort = z.enum(AUTO_EFFORTS);
|
|
26
|
-
const child = z.object({ model, reasoningEffort: effort }).strict();
|
|
27
|
-
const patchSchema = z.object({
|
|
28
|
-
...Object.fromEntries(Object.entries(ordinary).map(([key, schema]) => [key, schema.optional()])),
|
|
29
|
-
mainEfforts: z.partialRecord(provider, z.partialRecord(tier, effort.nullable()).nullable()).optional(),
|
|
30
|
-
childRoutes: z.partialRecord(provider, z.partialRecord(tier, child.nullable())).optional(),
|
|
31
|
-
}).strict();
|
|
32
|
-
const routeSchemas = {
|
|
33
|
-
mainEfforts: z.partialRecord(provider, z.partialRecord(tier, effort)),
|
|
34
|
-
childRoutes: z.partialRecord(provider, z.partialRecord(tier, child)),
|
|
18
|
+
effortCap: z.enum(AUTO_EFFORTS), jevTimeoutMs: z.number().int().min(1000).max(8000),
|
|
35
19
|
};
|
|
20
|
+
const patchSchema = z.object(Object.fromEntries(Object.entries(ordinary).map(([key, schema]) => [key, schema.optional()]))).strict();
|
|
36
21
|
export class SettingsValidationError extends Error {
|
|
37
22
|
issues;
|
|
38
23
|
constructor(issues) {
|
|
@@ -42,15 +27,11 @@ export class SettingsValidationError extends Error {
|
|
|
42
27
|
}
|
|
43
28
|
}
|
|
44
29
|
function schemaIssues(error, prefix = '') {
|
|
45
|
-
return error.issues.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return [];
|
|
50
|
-
return Object.entries(value.mainEfforts).flatMap(([provider, family]) => {
|
|
51
|
-
if (!['claude', 'codex'].includes(provider))
|
|
52
|
-
return [issue(`mainEfforts.${provider}`, 'invalid-setting', 'Invalid provider')];
|
|
53
|
-
return isRecord(family) ? Object.keys(family).filter(key => !tiers.includes(key)).map(key => issue(`mainEfforts.${provider}.${key}`, 'invalid-setting', 'Invalid tier')) : [];
|
|
30
|
+
return error.issues.flatMap(entry => {
|
|
31
|
+
// Unknown keys (retired mapping keys included) are reported by name.
|
|
32
|
+
if (entry.code === 'unrecognized_keys')
|
|
33
|
+
return entry.keys.map(key => issue([prefix, ...entry.path.map(String), key].filter(Boolean).join('.'), 'unknown-setting', '未知设置字段'));
|
|
34
|
+
return [issue([prefix, ...entry.path.map(String)].filter(Boolean).join('.'), 'invalid-setting', '设置字段无效')];
|
|
54
35
|
});
|
|
55
36
|
}
|
|
56
37
|
function credentialFields(value, path = '') {
|
|
@@ -63,127 +44,22 @@ function credentialFields(value, path = '') {
|
|
|
63
44
|
return credentialFields(entry, next);
|
|
64
45
|
});
|
|
65
46
|
}
|
|
66
|
-
/** Visit identifiable cells, never filtering malformed containers into an empty/default map. */
|
|
67
|
-
function cells(settings, visit) {
|
|
68
|
-
for (const kind of ['mainEfforts', 'childRoutes']) {
|
|
69
|
-
const map = settings[kind];
|
|
70
|
-
if (!isRecord(map))
|
|
71
|
-
continue;
|
|
72
|
-
for (const [provider, values] of Object.entries(map)) {
|
|
73
|
-
if (!isRecord(values))
|
|
74
|
-
continue;
|
|
75
|
-
for (const [tier, value] of Object.entries(values))
|
|
76
|
-
visit(kind, provider, tier, value, `${kind}.${provider}.${tier}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
export function validateSettingsRoutes(settings, catalog) {
|
|
81
|
-
const issues = [];
|
|
82
|
-
cells(settings, (kind, provider, tier, value, path) => {
|
|
83
|
-
if (!tiers.includes(tier))
|
|
84
|
-
return;
|
|
85
|
-
const explicitEffort = kind === 'mainEfforts' ? value : isRecord(value) ? value.reasoningEffort : undefined;
|
|
86
|
-
if (AUTO_EFFORTS.includes(explicitEffort) && AUTO_EFFORTS.indexOf(explicitEffort) > AUTO_EFFORTS.indexOf(settings.effortCap)) {
|
|
87
|
-
issues.push(issue(path, 'effort-cap', '超过推理强度上限'));
|
|
88
|
-
}
|
|
89
|
-
else
|
|
90
|
-
issues.push(...(kind === 'mainEfforts' ? validateFamilyMainEffort(catalog, provider, value, settings.effortCap, path) : validateChildRoute(catalog, provider, value, settings.effortCap, path)));
|
|
91
|
-
});
|
|
92
|
-
const main = settings.mainEfforts;
|
|
93
|
-
if (isRecord(main))
|
|
94
|
-
for (const [provider, family] of Object.entries(main)) {
|
|
95
|
-
if (!isRecord(family))
|
|
96
|
-
continue;
|
|
97
|
-
for (const [model, legacy] of Object.entries(family)) {
|
|
98
|
-
if (!isRecord(legacy))
|
|
99
|
-
continue;
|
|
100
|
-
for (const [tier, effort] of Object.entries(legacy))
|
|
101
|
-
if (tiers.includes(tier) && AUTO_EFFORTS.includes(effort) && AUTO_EFFORTS.indexOf(effort) > AUTO_EFFORTS.indexOf(settings.effortCap))
|
|
102
|
-
issues.push(issue(`mainEfforts.${provider}.${model}.${tier}`, 'effort-cap', '超过推理强度上限'));
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return issues;
|
|
106
|
-
}
|
|
107
|
-
/** Returns undefined only for a genuinely absent cell; malformed containers/cells throw. */
|
|
108
|
-
export function explicitMainEffort(settings, provider, model, tier, catalog) {
|
|
109
|
-
const family = explicitCell(settings.mainEfforts, [provider], 'mainEfforts');
|
|
110
|
-
if (family.present && (!isRecord(family.value) || Object.entries(family.value).some(([key, value]) => !tiers.includes(key) || isRecord(value)))) {
|
|
111
|
-
throw new SettingsValidationError([issue(`mainEfforts.${provider}`, 'invalid-route', '主会话映射必须按体系和档位配置,请替换旧模型映射或恢复默认')]);
|
|
112
|
-
}
|
|
113
|
-
const value = explicitCell(settings.mainEfforts, [provider, tier], 'mainEfforts');
|
|
114
|
-
if (!value.present)
|
|
115
|
-
return undefined;
|
|
116
|
-
const errors = validateMainRoute(catalog, provider, model, value.value, settings.effortCap, `mainEfforts.${provider}.${tier}`);
|
|
117
|
-
if (errors.length)
|
|
118
|
-
throw new SettingsValidationError(errors);
|
|
119
|
-
return value.value;
|
|
120
|
-
}
|
|
121
|
-
export function explicitChildRoute(settings, provider, tier, catalog) {
|
|
122
|
-
const value = explicitCell(settings.childRoutes, [provider, tier], 'childRoutes');
|
|
123
|
-
if (!value.present)
|
|
124
|
-
return undefined;
|
|
125
|
-
const errors = validateChildRoute(catalog, provider, value.value, settings.effortCap, `childRoutes.${provider}.${tier}`);
|
|
126
|
-
if (errors.length)
|
|
127
|
-
throw new SettingsValidationError(errors);
|
|
128
|
-
return value.value;
|
|
129
|
-
}
|
|
130
|
-
function explicitCell(root, keys, path) {
|
|
131
|
-
let value = root;
|
|
132
|
-
if (value === undefined)
|
|
133
|
-
return { present: false };
|
|
134
|
-
for (const key of keys) {
|
|
135
|
-
if (!isRecord(value))
|
|
136
|
-
throw new SettingsValidationError([issue(path, 'invalid-route', '显式路由配置格式无效')]);
|
|
137
|
-
if (!Object.hasOwn(value, key))
|
|
138
|
-
return { present: false };
|
|
139
|
-
value = value[key];
|
|
140
|
-
path += `.${key}`;
|
|
141
|
-
}
|
|
142
|
-
return { present: true, value };
|
|
143
|
-
}
|
|
144
|
-
function hasCellChanges(patch, depth) {
|
|
145
|
-
return Object.values(patch).some(value => depth === 1 || !isRecord(value) || hasCellChanges(value, depth - 1));
|
|
146
|
-
}
|
|
147
|
-
function mergeCells(current, patch, depth, path) {
|
|
148
|
-
if (!hasCellChanges(patch, depth))
|
|
149
|
-
return structuredClone(current);
|
|
150
|
-
if (current !== undefined && !isRecord(current))
|
|
151
|
-
throw new SettingsValidationError([issue(path, 'invalid-route', '显式路由容器格式无效,请先修复设置文件')]);
|
|
152
|
-
const merged = isRecord(current) ? structuredClone(current) : {};
|
|
153
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
154
|
-
if (value === null)
|
|
155
|
-
delete merged[key];
|
|
156
|
-
else if (depth > 1 && isRecord(value)) {
|
|
157
|
-
const previous = Object.hasOwn(merged, key) ? merged[key] : undefined;
|
|
158
|
-
// A nonempty canonical family submission explicitly replaces a legacy/malformed family.
|
|
159
|
-
const replaceFamily = path === 'mainEfforts' && Object.keys(value).length > 0 && (!isRecord(previous) || Object.entries(previous).some(([entry, value]) => !tiers.includes(entry) || isRecord(value)));
|
|
160
|
-
const nested = mergeCells(replaceFamily ? undefined : previous, value, depth - 1, `${path}.${key}`);
|
|
161
|
-
if (nested !== undefined && (!isRecord(nested) || Object.keys(nested).length))
|
|
162
|
-
Object.defineProperty(merged, key, { value: nested, enumerable: true, configurable: true, writable: true });
|
|
163
|
-
else
|
|
164
|
-
delete merged[key];
|
|
165
|
-
}
|
|
166
|
-
else
|
|
167
|
-
Object.defineProperty(merged, key, { value: structuredClone(value), enumerable: true, configurable: true, writable: true });
|
|
168
|
-
}
|
|
169
|
-
return merged;
|
|
170
|
-
}
|
|
171
47
|
export class SettingsStore {
|
|
172
48
|
path;
|
|
173
49
|
constructor(path) {
|
|
174
50
|
this.path = path;
|
|
175
51
|
}
|
|
176
|
-
read(
|
|
52
|
+
read() {
|
|
177
53
|
let raw;
|
|
178
54
|
try {
|
|
179
55
|
raw = JSON.parse(readFileSync(this.path, 'utf8'));
|
|
180
56
|
}
|
|
181
57
|
catch (error) {
|
|
182
58
|
const missing = error.code === 'ENOENT';
|
|
183
|
-
return { settings: { ...DEFAULT_SETTINGS }, status: missing ? 'missing' : 'invalid', issues: missing ? [] : [issue('', 'invalid-file', '设置文件无法读取或解析,保存将覆盖它')] };
|
|
59
|
+
return { settings: { ...DEFAULT_SETTINGS }, status: missing ? 'missing' : 'invalid', deprecated: [], issues: missing ? [] : [issue('', 'invalid-file', '设置文件无法读取或解析,保存将覆盖它')] };
|
|
184
60
|
}
|
|
185
61
|
if (!isRecord(raw))
|
|
186
|
-
return { settings: { ...DEFAULT_SETTINGS }, status: 'invalid', issues: [issue('', 'invalid-file', '设置文件必须是对象')] };
|
|
62
|
+
return { settings: { ...DEFAULT_SETTINGS }, status: 'invalid', deprecated: [], issues: [issue('', 'invalid-file', '设置文件必须是对象')] };
|
|
187
63
|
const settings = { ...DEFAULT_SETTINGS };
|
|
188
64
|
const issues = [];
|
|
189
65
|
for (const [key, schema] of Object.entries(ordinary)) {
|
|
@@ -199,28 +75,18 @@ export class SettingsStore {
|
|
|
199
75
|
Object.assign(settings, { decisionService: { provider: 'custom', endpoint: '' } });
|
|
200
76
|
}
|
|
201
77
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
continue;
|
|
205
|
-
Object.assign(settings, { [kind]: raw[kind] });
|
|
206
|
-
const parsed = routeSchemas[kind].safeParse(raw[kind]);
|
|
207
|
-
if (!parsed.success)
|
|
208
|
-
issues.push(...schemaIssues(parsed.error, kind));
|
|
209
|
-
}
|
|
210
|
-
issues.push(...mainKeyIssues(raw));
|
|
211
|
-
if (catalog)
|
|
212
|
-
issues.push(...validateSettingsRoutes(settings, catalog));
|
|
213
|
-
return { settings, issues, status: issues.length ? 'invalid' : 'ok' };
|
|
78
|
+
const deprecated = DEPRECATED_KEYS.filter(key => Object.hasOwn(raw, key));
|
|
79
|
+
return { settings, issues, status: issues.length ? 'invalid' : 'ok', deprecated };
|
|
214
80
|
}
|
|
215
|
-
save(patch
|
|
81
|
+
save(patch) {
|
|
216
82
|
const parsed = patchSchema.safeParse(patch);
|
|
217
|
-
const errors = [...credentialFields(patch), ...
|
|
83
|
+
const errors = [...credentialFields(patch), ...(!parsed.success ? schemaIssues(parsed.error) : [])];
|
|
218
84
|
if (errors.length)
|
|
219
85
|
throw new SettingsValidationError(errors);
|
|
220
86
|
const submitted = patch;
|
|
221
87
|
const before = this.read();
|
|
222
88
|
const persisted = {};
|
|
223
|
-
// Only
|
|
89
|
+
// Only ordinary fields already in the file or submitted now are written; retired keys are dropped.
|
|
224
90
|
let disk;
|
|
225
91
|
try {
|
|
226
92
|
disk = JSON.parse(readFileSync(this.path, 'utf8'));
|
|
@@ -231,31 +97,6 @@ export class SettingsStore {
|
|
|
231
97
|
for (const key of Object.keys(ordinary))
|
|
232
98
|
if ((isRecord(disk) && Object.hasOwn(disk, key)) || Object.hasOwn(submitted, key))
|
|
233
99
|
persisted[key] = submitted[key] ?? before.settings[key];
|
|
234
|
-
for (const kind of ['mainEfforts', 'childRoutes']) {
|
|
235
|
-
const existing = before.settings[kind];
|
|
236
|
-
if (Object.hasOwn(submitted, kind)) {
|
|
237
|
-
const merged = mergeCells(existing, submitted[kind], 2, kind);
|
|
238
|
-
if (merged !== undefined && (!isRecord(merged) || Object.keys(merged).length))
|
|
239
|
-
persisted[kind] = merged;
|
|
240
|
-
}
|
|
241
|
-
else if (existing !== undefined)
|
|
242
|
-
persisted[kind] = existing;
|
|
243
|
-
}
|
|
244
|
-
const next = { ...DEFAULT_SETTINGS, ...persisted };
|
|
245
|
-
const capChanged = next.effortCap !== before.settings.effortCap;
|
|
246
|
-
const touched = new Set();
|
|
247
|
-
const previous = new Map();
|
|
248
|
-
cells(before.settings, (_kind, _provider, _tier, value, path) => { previous.set(path, value); });
|
|
249
|
-
cells(submitted, (_kind, _provider, _tier, value, path) => {
|
|
250
|
-
const oldMain = before.settings.mainEfforts;
|
|
251
|
-
const oldFamily = isRecord(oldMain) ? oldMain[_provider] : undefined;
|
|
252
|
-
const replacesFamily = _kind === 'mainEfforts' && oldFamily !== undefined && (!isRecord(oldFamily) || Object.entries(oldFamily).some(([key, value]) => !tiers.includes(key) || isRecord(value)));
|
|
253
|
-
if (value !== null && (replacesFamily || JSON.stringify(value) !== JSON.stringify(previous.get(path))))
|
|
254
|
-
touched.add(path);
|
|
255
|
-
});
|
|
256
|
-
const validation = validateSettingsRoutes(next, catalog).filter(entry => touched.has(entry.path) || (capChanged && entry.code === 'effort-cap'));
|
|
257
|
-
if (validation.length)
|
|
258
|
-
throw new SettingsValidationError(validation);
|
|
259
100
|
const directory = dirname(this.path);
|
|
260
101
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
261
102
|
chmodSync(directory, 0o700);
|
|
@@ -272,6 +113,6 @@ export class SettingsStore {
|
|
|
272
113
|
catch { }
|
|
273
114
|
throw error;
|
|
274
115
|
}
|
|
275
|
-
return this.read(
|
|
116
|
+
return this.read();
|
|
276
117
|
}
|
|
277
118
|
}
|
package/lib/state.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Effort
|
|
1
|
+
import type { Effort } from './effort.js';
|
|
2
2
|
import type { JevCategory } from './jev.js';
|
|
3
3
|
export type Mode = 'AUTO' | 'MANUAL';
|
|
4
4
|
export interface Selection {
|
|
@@ -12,7 +12,7 @@ export interface Disabled {
|
|
|
12
12
|
}
|
|
13
13
|
export interface SessionState {
|
|
14
14
|
mode: Mode;
|
|
15
|
-
previous?:
|
|
15
|
+
previous?: 'simple' | 'standard' | 'complex' | 'escalated';
|
|
16
16
|
disabled?: Disabled;
|
|
17
17
|
}
|
|
18
18
|
export interface StateStore {
|
|
@@ -33,7 +33,5 @@ export declare class RoutingState {
|
|
|
33
33
|
setMode(id: string, mode: Mode): SessionState;
|
|
34
34
|
/** Jev failed: switch to MANUAL (no restore) and keep the visible reason. */
|
|
35
35
|
disable(id: string, category: JevCategory, at?: number): SessionState;
|
|
36
|
-
/** Keep the latest user-turn tier (name only) so a restart still sends it to Jev as context. */
|
|
37
|
-
remember(id: string, tier: Tier): void;
|
|
38
36
|
private save;
|
|
39
37
|
}
|
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;
|