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.
Files changed (75) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +15 -10
  3. package/README.zh.md +15 -9
  4. package/docs/feedback-evaluation.md +20 -3
  5. package/docs/host-capability-report.md +2 -0
  6. package/docs/jev-policy-v2-validation.md +3 -1
  7. package/docs/magpie-live-acceptance.md +2 -0
  8. package/docs/policy-v2-development.md +5 -3
  9. package/docs/policy-v2-real-evaluation-plan.md +2 -0
  10. package/lib/client/control.d.ts +2 -2
  11. package/lib/client/control.js +10 -20
  12. package/lib/client/index.d.ts +9 -2
  13. package/lib/client/index.js +97 -12
  14. package/lib/client/label.d.ts +8 -11
  15. package/lib/client/label.js +51 -29
  16. package/lib/client/settings-section.d.ts +2 -2
  17. package/lib/client/settings-section.js +7 -69
  18. package/lib/client/strategy-details.js +3 -4
  19. package/lib/client.js +316 -489
  20. package/lib/context.d.ts +1 -3
  21. package/lib/context.js +3 -4
  22. package/lib/effort.d.ts +2 -10
  23. package/lib/effort.js +1 -31
  24. package/lib/endpoints.d.ts +11 -9
  25. package/lib/endpoints.js +44 -42
  26. package/lib/feedback.d.ts +77 -6
  27. package/lib/feedback.js +34 -3
  28. package/lib/index.d.ts +4 -0
  29. package/lib/index.js +54 -89
  30. package/lib/jev.d.ts +17 -29
  31. package/lib/jev.js +6 -34
  32. package/lib/policy-host.d.ts +13 -8
  33. package/lib/policy-host.js +98 -47
  34. package/lib/policy-store.d.ts +9 -17
  35. package/lib/policy-store.js +28 -15
  36. package/lib/routing.d.ts +20 -0
  37. package/lib/routing.js +51 -0
  38. package/lib/rubric.json +15 -6
  39. package/lib/settings-contract.d.ts +5 -21
  40. package/lib/settings-contract.js +3 -2
  41. package/lib/settings-host.d.ts +4 -8
  42. package/lib/settings-host.js +9 -9
  43. package/lib/settings-validation.d.ts +1 -7
  44. package/lib/settings-validation.js +0 -51
  45. package/lib/settings.d.ts +3 -8
  46. package/lib/settings.js +18 -177
  47. package/lib/state.d.ts +2 -4
  48. package/lib/state.js +2 -7
  49. package/lib/strategies/claude.d.ts +21 -0
  50. package/lib/strategies/claude.js +8 -0
  51. package/lib/strategies/codex.d.ts +21 -0
  52. package/lib/strategies/codex.js +9 -0
  53. package/lib/strategies/index.d.ts +22 -0
  54. package/lib/strategies/index.js +85 -0
  55. package/lib/strategies/types.d.ts +14 -0
  56. package/lib/strategies/types.js +1 -0
  57. package/lib/subagent-tool.d.ts +7 -7
  58. package/lib/subagent-tool.js +33 -76
  59. package/lib/support.d.ts +24 -0
  60. package/lib/support.js +36 -0
  61. package/lib/usage-report.d.ts +21 -2
  62. package/lib/usage-report.js +86 -22
  63. package/package.json +19 -7
  64. package/skills/auto-model-routing-report/SKILL.md +9 -3
  65. package/skills/subagent-model-routing/SKILL.md +14 -30
  66. package/lib/client/route-picker.d.ts +0 -21
  67. package/lib/client/route-picker.js +0 -75
  68. package/lib/judgements.d.ts +0 -41
  69. package/lib/judgements.js +0 -55
  70. package/lib/policy-v2-rubric.json +0 -21
  71. package/lib/policy-v2.d.ts +0 -20
  72. package/lib/policy-v2.js +0 -53
  73. package/lib/router.d.ts +0 -68
  74. package/lib/router.js +0 -153
  75. package/scripts/label-report.mjs +0 -50
@@ -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: (includeMagpie?: boolean) => Promise<RoutingCatalog>;
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;
@@ -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 rubric from './rubric.json' with { type: 'json' };
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 (includeMagpie = store.read().settings.policy === 'policy-v2') => {
13
- const models = (await Promise.all(ctx.llm.listProviders().filter(p => p.id === 'claude' || p.id === 'codex' || (includeMagpie && p.id === 'dsh-magpie-connect')).map(async (provider) => {
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 => r.provider === 'claude' || r.provider === 'codex' || (includeMagpie && r.provider === 'dsh-magpie-connect')) } };
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(current);
56
- return { settings: result.settings, defaults: DEFAULT_SETTINGS, configStatus: result.status, issues: result.issues, key: await keyStatus(), ...current, criteria: rubric.tier.criteria };
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, 4500, () => store.read().settings.decisionService);
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, await catalog());
68
+ store.save(body);
69
69
  return Response.json(await read());
70
70
  }
71
71
  catch (error) {
@@ -1,10 +1,4 @@
1
- import { type AutomaticEffort, type RoutingCatalog, type RoutingModel, type SettingsIssue } from './settings-contract.js';
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 Tier } from './effort.js';
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(catalog?: RoutingCatalog): SettingsRead;
17
- save(patch: unknown, catalog: RoutingCatalog): SettingsRead;
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 { tiers } from './effort.js';
7
- import { AUTO_EFFORTS, DEFAULT_SETTINGS } from './settings-contract.js';
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(4500),
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.map(entry => issue([prefix, ...entry.path.map(String)].filter(Boolean).join('.'), 'invalid-setting', '设置字段无效'));
46
- }
47
- function mainKeyIssues(value) {
48
- if (!isRecord(value) || !isRecord(value.mainEfforts))
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(catalog) {
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
- for (const kind of ['mainEfforts', 'childRoutes']) {
203
- if (!Object.hasOwn(raw, kind))
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, catalog) {
81
+ save(patch) {
216
82
  const parsed = patchSchema.safeParse(patch);
217
- const errors = [...credentialFields(patch), ...mainKeyIssues(patch), ...(!parsed.success ? schemaIssues(parsed.error) : [])];
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 persist existing scalar fields; ordinary invalid values are normalized on an explicit save.
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(catalog);
116
+ return this.read();
276
117
  }
277
118
  }
package/lib/state.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Effort, Tier } from './effort.js';
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?: Tier;
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
- // Legacy files also carried `restore`/`anchor`; they are ignored (non-strict) and dropped on the next write.
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;