dsh-autotier 0.1.0 → 0.1.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 CHANGED
@@ -5,6 +5,32 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.1] - 2026-09-09
9
+
10
+ ### Added
11
+
12
+ - **Route pre-flight against the live registry.** Before a tier landing is
13
+ written into the request configuration, the plugin now checks the two facts
14
+ that would fail the request outright: the provider route must be registered,
15
+ and the target model must declare the configured reasoning effort. An
16
+ unregistered provider redirects to a registered fallback chain entry; an
17
+ unsupported effort is dropped so the adapter default applies; an adapter that
18
+ cannot answer is treated leniently. Both answers are cached per
19
+ `provider/model`, and each degradation is logged once.
20
+ - Compat workflow now runs its profile job over a matrix of both published
21
+ harness lines (`0.1.2-rc.1` and `0.1.5-alpha.1`), so drift in either is caught
22
+ automatically.
23
+
24
+ ### Changed
25
+
26
+ - Peer ranges name both published lines explicitly
27
+ (`>=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0`): a semver range whose only
28
+ prerelease comparator sits on an earlier version tuple does not admit a later
29
+ alpha, so the previous range left `0.1.5-alpha.1` unsatisfied.
30
+ - `dsh.compatibility.dshReleases` and the five READMEs now declare
31
+ `0.1.5-alpha.1` compatible after an end-to-end verification (real profile
32
+ install, `--dump-config` row, keyless headless smoke).
33
+
8
34
  ## [0.1.0] - 2026-09-09
9
35
 
10
36
  ### Added
package/README.es.md CHANGED
@@ -19,10 +19,14 @@ strong con un TTL de retorno.
19
19
 
20
20
  | Harness | Estado |
21
21
  |---|---|
22
- | `@deepseek-ai/dsh` `0.1.2-rc.1` | compatible (es lo que CI verifica y lo que instala el flujo compat) |
23
- | `0.1.5-alpha.1` (la línea actual del checkout) | verificado por tipos contra sus caras publicadas; el smoke de extremo a extremo corre en `0.1.2-rc.1` |
22
+ | `@deepseek-ai/dsh` `0.1.2-rc.1` | compatible; el flujo compat instala esta línea de extremo a extremo |
23
+ | `@deepseek-ai/dsh` `0.1.5-alpha.1` | compatible; verificada de extremo a extremo (perfil real, fila en `--dump-config`, smoke keyless) y en la matriz compat |
24
24
  | `@deepseek-ai/cordis` `^4.0.2`, `@deepseek-ai/schemastery` `^3.18.2` | base de peers |
25
25
 
26
+ Los rangos de peers nombran ambas líneas publicadas (`>=0.1.2-rc.1 <0.2.0 ||
27
+ >=0.1.5-alpha.1 <0.2.0`), porque un rango cuyo único comparador de prerelease
28
+ está en una tupla anterior no admite un alpha posterior. Se refrescan por ola.
29
+
26
30
  El plugin vive solo en el plano host y no necesita un preset propio: la fila host
27
31
  se aplica a todas las sesiones. Una sección de prompt en *tu* preset es opcional
28
32
  y solo hace visibles las decisiones al modelo (véase [Instalación y desinstalación](#instalación-y-desinstalación)).
package/README.hi.md CHANGED
@@ -17,10 +17,15 @@ DeepSeek Harness के लिए स्वचालित मॉडल-स्
17
17
 
18
18
  | Harness | स्थिति |
19
19
  |---|---|
20
- | `@deepseek-ai/dsh` `0.1.2-rc.1` | संगत (CI इसी की जाँच करता है और compat वर्कफ़्लो इसे इंस्टॉल करता है) |
21
- | `0.1.5-alpha.1` (वर्तमान checkout लाइन) | इसके प्रकाशित type faces के विरुद्ध टाइप-जाँच; end-to-end smoke `0.1.2-rc.1` पर चलता है |
20
+ | `@deepseek-ai/dsh` `0.1.2-rc.1` | संगत; compat वर्कफ़्लो इस लाइन को end-to-end इंस्टॉल करता है |
21
+ | `@deepseek-ai/dsh` `0.1.5-alpha.1` | संगत; end-to-end सत्यापित (वास्तविक profile, `--dump-config` पंक्ति, keyless smoke) और compat मैट्रिक्स में |
22
22
  | `@deepseek-ai/cordis` `^4.0.2`, `@deepseek-ai/schemastery` `^3.18.2` | peer आधार |
23
23
 
24
+ peer ranges दोनों प्रकाशित लाइनें स्पष्ट रूप से लिखती हैं (`>=0.1.2-rc.1 <0.2.0
25
+ || >=0.1.5-alpha.1 <0.2.0`), क्योंकि जिस range का एकमात्र prerelease comparator
26
+ पुराने tuple पर हो वह बाद के alpha को स्वीकार नहीं करता। हर प्रकाशन-लहर पर
27
+ रिफ़्रेश होता है।
28
+
24
29
  यह प्लगइन केवल host plane पर रहता है और अपना preset नहीं माँगता: host पंक्ति
25
30
  हर सत्र पर लागू होती है। *आपके* preset में एक प्रॉम्प्ट खंड वैकल्पिक है और
26
31
  केवल मॉडल को निर्णय दिखाता है ([इंस्टॉल और अनइंस्टॉल](#इंस्टॉल-और-अनइंस्टॉल) देखें)।
package/README.md CHANGED
@@ -17,10 +17,15 @@ escalate to the strong tier with a TTL fallback.
17
17
 
18
18
  | Harness | Status |
19
19
  |---|---|
20
- | `@deepseek-ai/dsh` `0.1.2-rc.1` | compatible (this is what CI type-checks and the compat workflow installs) |
21
- | `0.1.5-alpha.1` (the current harness checkout line) | type-checked against its published type faces; the end-to-end smoke runs on `0.1.2-rc.1` |
20
+ | `@deepseek-ai/dsh` `0.1.2-rc.1` | compatible; the compat workflow installs this line end-to-end |
21
+ | `@deepseek-ai/dsh` `0.1.5-alpha.1` | compatible; verified end-to-end (real profile install, `--dump-config` row, keyless headless smoke) and in the compat matrix |
22
22
  | `@deepseek-ai/cordis` `^4.0.2`, `@deepseek-ai/schemastery` `^3.18.2` | peer baseline |
23
23
 
24
+ Peer ranges name both published lines explicitly (`>=0.1.2-rc.1 <0.2.0 ||
25
+ >=0.1.5-alpha.1 <0.2.0`), because a semver range whose only prerelease
26
+ comparator sits on an earlier version tuple does not admit a later alpha.
27
+ They are refreshed per published wave.
28
+
24
29
  The plugin is host-plane only. It needs no agent preset of its own: the host
25
30
  row applies to every session. A one-line prompt section in *your* preset is
26
31
  optional and only makes the router's decisions visible to the model (see
package/README.pt.md CHANGED
@@ -18,10 +18,14 @@ retorno.
18
18
 
19
19
  | Harness | Estado |
20
20
  |---|---|
21
- | `@deepseek-ai/dsh` `0.1.2-rc.1` | compatível o que a CI verifica e o que o fluxo compat instala) |
22
- | `0.1.5-alpha.1` (a linha atual do checkout) | verificado por tipos contra suas faces publicadas; o smoke ponta a ponta roda em `0.1.2-rc.1` |
21
+ | `@deepseek-ai/dsh` `0.1.2-rc.1` | compatível; o fluxo compat instala esta linha ponta a ponta |
22
+ | `@deepseek-ai/dsh` `0.1.5-alpha.1` | compatível; verificada ponta a ponta (perfil real, linha no `--dump-config`, smoke keyless) e na matriz compat |
23
23
  | `@deepseek-ai/cordis` `^4.0.2`, `@deepseek-ai/schemastery` `^3.18.2` | base de peers |
24
24
 
25
+ Os ranges de peers nomeiam as duas linhas publicadas (`>=0.1.2-rc.1 <0.2.0 ||
26
+ >=0.1.5-alpha.1 <0.2.0`), porque um range cujo único comparador de prerelease
27
+ está numa tupla anterior não admite um alpha posterior. São atualizados por onda.
28
+
25
29
  O plugin vive apenas no plano host e não precisa de preset próprio: a linha host
26
30
  vale para todas as sessões. Uma seção de prompt no *seu* preset é opcional e
27
31
  apenas torna as decisões visíveis ao modelo (veja [Instalação e desinstalação](#instalação-e-desinstalação)).
package/README.zh.md CHANGED
@@ -14,10 +14,14 @@ DeepSeek Harness 的自动模型分档路由:一条用户指令进来,一个
14
14
 
15
15
  | Harness | 状态 |
16
16
  |---|---|
17
- | `@deepseek-ai/dsh` `0.1.2-rc.1` | 兼容(CI 的类型检查与 compat 工作流安装的就是它) |
18
- | `0.1.5-alpha.1`(当前宿主 checkout 线) | 对照其已发布类型面做类型检查;端到端冒烟跑在 `0.1.2-rc.1` |
17
+ | `@deepseek-ai/dsh` `0.1.2-rc.1` | 兼容;compat 工作流对该线做端到端安装验证 |
18
+ | `@deepseek-ai/dsh` `0.1.5-alpha.1` | 兼容;已端到端实测(真实 profile 安装、`--dump-config` 行、keyless headless 冒烟)并纳入 compat 矩阵 |
19
19
  | `@deepseek-ai/cordis` `^4.0.2`、`@deepseek-ai/schemastery` `^3.18.2` | peer 基线 |
20
20
 
21
+ peer 范围显式列出两条已发布线(`>=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1
22
+ <0.2.0`)——因为仅含较早版本元组预发布比较符的 semver 范围无法接纳更晚的 alpha;
23
+ 每次发布波同步刷新。
24
+
21
25
  本插件只驻留 host 平面,不需要自带 agent preset:host 行对所有会话生效。
22
26
  在你的 preset 中加一段提示是可选项,仅用于让模型看见路由决策
23
27
  (见[安装与卸载](#安装与卸载))。
package/lib/index.js CHANGED
@@ -2078,6 +2078,82 @@ async function runJudge(ctx, config, text, signal) {
2078
2078
  };
2079
2079
  }
2080
2080
  //#endregion
2081
+ //#region src/preflight.ts
2082
+ /**
2083
+ * Pre-flight cache and checker for one plugin instance. The cache is per
2084
+ * process and never invalidated: adapter registrations are composition-level
2085
+ * facts, and a reload rebuilds the plugin (and this cache) with them.
2086
+ */
2087
+ var RoutePreflight = class {
2088
+ ctx;
2089
+ onDegrade;
2090
+ capabilities = /* @__PURE__ */ new Map();
2091
+ degraded = /* @__PURE__ */ new Set();
2092
+ /** @param options - the plugin context and the degrade reporter. */
2093
+ constructor(options) {
2094
+ this.ctx = options.ctx;
2095
+ this.onDegrade = options.onDegrade;
2096
+ }
2097
+ /** One cached capability lookup; a failing adapter is treated as lenient. */
2098
+ capability(provider, model) {
2099
+ const key = `${provider}/${model}`;
2100
+ let pending = this.capabilities.get(key);
2101
+ if (pending === void 0) {
2102
+ pending = this.ctx.llm.listProviders().some((entry) => entry.id === provider) ? this.ctx.llm.resolveModelInfo(provider, model).then((info) => ({
2103
+ registered: true,
2104
+ efforts: info.reasoning === void 0 ? void 0 : new Set(info.reasoning.efforts.map((effort) => String(effort.id)))
2105
+ }), () => ({
2106
+ registered: true,
2107
+ efforts: void 0
2108
+ })) : Promise.resolve({
2109
+ registered: false,
2110
+ efforts: void 0
2111
+ });
2112
+ this.capabilities.set(key, pending);
2113
+ }
2114
+ return pending;
2115
+ }
2116
+ /** Report one degradation at most once per key. */
2117
+ degrade(key, note) {
2118
+ if (this.degraded.has(key)) return;
2119
+ this.degraded.add(key);
2120
+ this.onDegrade(key, note);
2121
+ }
2122
+ /**
2123
+ * Make one tier landing safe to apply.
2124
+ * @param route - the landing the policy produced.
2125
+ * @param tier - the tier configuration, for its fallback chain.
2126
+ * @returns a landing that will not fail the request on provider/effort facts.
2127
+ */
2128
+ async sanitize(route, tier) {
2129
+ const capability = await this.capability(route.provider, route.model);
2130
+ let safe = route;
2131
+ if (!capability.registered) {
2132
+ const entry = tier.fallback.find((candidate) => this.ctx.llm.listProviders().some((p) => p.id === candidate.provider));
2133
+ if (entry !== void 0) {
2134
+ this.degrade(`provider:${route.provider}`, `provider "${route.provider}" is not registered; using fallback "${entry.provider}/${entry.model}"`);
2135
+ safe = tier.followSession ? {
2136
+ provider: entry.provider,
2137
+ model: entry.model
2138
+ } : {
2139
+ provider: entry.provider,
2140
+ model: entry.model,
2141
+ effort: tier.effort
2142
+ };
2143
+ } else this.degrade(`provider:${route.provider}`, `provider "${route.provider}" is not registered and the tier has no usable fallback`);
2144
+ }
2145
+ const effort = safe.effort;
2146
+ if (effort !== void 0 && capability.efforts !== void 0 && !capability.efforts.has(effort)) {
2147
+ this.degrade(`effort:${route.provider}/${route.model}`, `model "${route.model}" does not accept reasoning effort "${effort}"; the adapter default applies`);
2148
+ safe = {
2149
+ provider: safe.provider,
2150
+ model: safe.model
2151
+ };
2152
+ }
2153
+ return safe;
2154
+ }
2155
+ };
2156
+ //#endregion
2081
2157
  //#region src/routing.ts
2082
2158
  /** How long third parties have to veto a proposed tier before the turn proceeds. */
2083
2159
  const VETO_TIMEOUT_MS = 250;
@@ -2106,6 +2182,7 @@ var AutotierRouter = class {
2106
2182
  counters = /* @__PURE__ */ new WeakMap();
2107
2183
  /** Router-owned lifetime signal: aborts in-flight judge calls on unload. */
2108
2184
  lifetime = new AbortController();
2185
+ preflight;
2109
2186
  disposed = false;
2110
2187
  /**
2111
2188
  * Register every listener on the plugin fiber.
@@ -2115,6 +2192,10 @@ var AutotierRouter = class {
2115
2192
  this.ctx = options.ctx;
2116
2193
  this.service = options.service;
2117
2194
  this.states = options.states;
2195
+ this.preflight = new RoutePreflight({
2196
+ ctx: this.ctx,
2197
+ onDegrade: (_key, note) => this.ctx.logger.warn("dsh-autotier: %s", note)
2198
+ });
2118
2199
  this.ctx.on("agent/inbox/inserted", (payload) => this.onInboxInserted(payload.agent, payload.message), { prepend: true });
2119
2200
  this.ctx.on("agent/request", (payload, next) => this.onRequest(payload.agent, payload.turn, payload.step, next), { prepend: true });
2120
2201
  this.ctx.on("agent/error", (payload) => this.onAgentError(payload.agent, payload.error));
@@ -2377,7 +2458,7 @@ var AutotierRouter = class {
2377
2458
  const source = veto === void 0 || veto === null ? decision.source : "manual";
2378
2459
  const reason = veto === void 0 || veto === null ? decision.reason : `veto: ${veto.reason}`;
2379
2460
  const route = this.routeFor(tier, config, intent, state, now, base);
2380
- const applied = resolveRoute(base, route);
2461
+ const applied = resolveRoute(base, await this.preflight.sanitize(route, tier === "strong" ? config.tiers.strong : config.tiers.cheap));
2381
2462
  if (state.appliedTier !== tier) {
2382
2463
  const from = state.appliedTier;
2383
2464
  state.appliedTier = tier;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Request-time route pre-flight: before a tier landing is written into the
3
+ * request configuration, check the two facts that would make the request fail
4
+ * outright — is the provider route registered, and does the target model accept
5
+ * the configured reasoning effort?
6
+ *
7
+ * Both answers come from the live `ctx.llm` registry and are cached per
8
+ * `provider/model`, so the common case costs one Map lookup. A route that fails
9
+ * pre-flight degrades instead of failing the turn: an unregistered provider
10
+ * falls back to a registered chain entry, and an unsupported effort is omitted
11
+ * (the adapter default then applies). Each degradation is logged once per key.
12
+ *
13
+ * @module dsh-autotier/preflight
14
+ */
15
+ import type { Context } from '@deepseek-ai/cordis';
16
+ import type { ResolvedTierConfig } from './config.js';
17
+ import type { TierRoute } from './types.js';
18
+ /** Options for {@link RoutePreflight}. */
19
+ export interface RoutePreflightOptions {
20
+ readonly ctx: Context;
21
+ /** Called once per degraded route key; the caller owns the log level. */
22
+ readonly onDegrade: (key: string, note: string) => void;
23
+ }
24
+ /**
25
+ * Pre-flight cache and checker for one plugin instance. The cache is per
26
+ * process and never invalidated: adapter registrations are composition-level
27
+ * facts, and a reload rebuilds the plugin (and this cache) with them.
28
+ */
29
+ export declare class RoutePreflight {
30
+ private readonly ctx;
31
+ private readonly onDegrade;
32
+ private readonly capabilities;
33
+ private readonly degraded;
34
+ /** @param options - the plugin context and the degrade reporter. */
35
+ constructor(options: RoutePreflightOptions);
36
+ /** One cached capability lookup; a failing adapter is treated as lenient. */
37
+ private capability;
38
+ /** Report one degradation at most once per key. */
39
+ private degrade;
40
+ /**
41
+ * Make one tier landing safe to apply.
42
+ * @param route - the landing the policy produced.
43
+ * @param tier - the tier configuration, for its fallback chain.
44
+ * @returns a landing that will not fail the request on provider/effort facts.
45
+ */
46
+ sanitize(route: TierRoute, tier: ResolvedTierConfig): Promise<TierRoute>;
47
+ }
48
+ //# sourceMappingURL=preflight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preflight.d.ts","sourceRoot":"","sources":["../../src/preflight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAK3C,0CAA0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IACrB,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACxD;AAED;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;IAC/D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyC;IACtE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAE7C,oEAAoE;gBACxD,OAAO,EAAE,qBAAqB;IAK1C,6EAA6E;IAC7E,OAAO,CAAC,UAAU;IAiBlB,mDAAmD;IACnD,OAAO,CAAC,OAAO;IAMf;;;;;OAKG;IACG,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,SAAS,CAAC;CA2B/E"}
@@ -71,6 +71,7 @@ export declare class AutotierRouter {
71
71
  private readonly counters;
72
72
  /** Router-owned lifetime signal: aborts in-flight judge calls on unload. */
73
73
  private readonly lifetime;
74
+ private readonly preflight;
74
75
  private disposed;
75
76
  /**
76
77
  * Register every listener on the plugin fiber.
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/routing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/routing.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-autotier",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Automatic strong/cheap model-tier routing for DeepSeek Harness: intent-gated tier landing on the agent/request waterfall, plan-mode handoff (strong plans, cheap implements), deterministic high-risk guards on tools/pre-execute, failure escalation with TTL fallback, a /tier command, and tier status tools",
5
5
  "repository": {
6
6
  "type": "git",
@@ -44,7 +44,8 @@
44
44
  },
45
45
  "compatibility": {
46
46
  "dshReleases": {
47
- "0.1.2-rc.1": "compatible"
47
+ "0.1.2-rc.1": "compatible",
48
+ "0.1.5-alpha.1": "compatible"
48
49
  }
49
50
  }
50
51
  },
@@ -104,14 +105,14 @@
104
105
  "packageManager": "pnpm@11.7.0",
105
106
  "peerDependencies": {
106
107
  "@deepseek-ai/cordis": "^4.0.2",
107
- "@deepseek-ai/dsh-agent": ">=0.1.2-rc.1 <0.2.0",
108
- "@deepseek-ai/dsh-commands": ">=0.1.2-rc.1 <0.2.0",
109
- "@deepseek-ai/dsh-llm": ">=0.1.2-rc.1 <0.2.0",
110
- "@deepseek-ai/dsh-plan-mode": ">=0.1.2-rc.1 <0.2.0",
111
- "@deepseek-ai/dsh-session": ">=0.1.2-rc.1 <0.2.0",
112
- "@deepseek-ai/dsh-session-projection": ">=0.1.2-rc.1 <0.2.0",
113
- "@deepseek-ai/dsh-settings": ">=0.1.2-rc.1 <0.2.0",
114
- "@deepseek-ai/dsh-tools": ">=0.1.2-rc.1 <0.2.0",
108
+ "@deepseek-ai/dsh-agent": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0",
109
+ "@deepseek-ai/dsh-commands": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0",
110
+ "@deepseek-ai/dsh-llm": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0",
111
+ "@deepseek-ai/dsh-plan-mode": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0",
112
+ "@deepseek-ai/dsh-session": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0",
113
+ "@deepseek-ai/dsh-session-projection": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0",
114
+ "@deepseek-ai/dsh-settings": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0",
115
+ "@deepseek-ai/dsh-tools": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0",
115
116
  "@deepseek-ai/schemastery": "^3.18.2"
116
117
  },
117
118
  "peerDependenciesMeta": {
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Request-time route pre-flight: before a tier landing is written into the
3
+ * request configuration, check the two facts that would make the request fail
4
+ * outright — is the provider route registered, and does the target model accept
5
+ * the configured reasoning effort?
6
+ *
7
+ * Both answers come from the live `ctx.llm` registry and are cached per
8
+ * `provider/model`, so the common case costs one Map lookup. A route that fails
9
+ * pre-flight degrades instead of failing the turn: an unregistered provider
10
+ * falls back to a registered chain entry, and an unsupported effort is omitted
11
+ * (the adapter default then applies). Each degradation is logged once per key.
12
+ *
13
+ * @module dsh-autotier/preflight
14
+ */
15
+
16
+ import type { Context } from '@deepseek-ai/cordis'
17
+ import type { ResolvedTierConfig } from './config.ts'
18
+ import type { TierRoute } from './types.ts'
19
+
20
+ /** One cached model capability answer. */
21
+ type Capability = { readonly registered: boolean; readonly efforts: ReadonlySet<string> | undefined }
22
+
23
+ /** Options for {@link RoutePreflight}. */
24
+ export interface RoutePreflightOptions {
25
+ readonly ctx: Context
26
+ /** Called once per degraded route key; the caller owns the log level. */
27
+ readonly onDegrade: (key: string, note: string) => void
28
+ }
29
+
30
+ /**
31
+ * Pre-flight cache and checker for one plugin instance. The cache is per
32
+ * process and never invalidated: adapter registrations are composition-level
33
+ * facts, and a reload rebuilds the plugin (and this cache) with them.
34
+ */
35
+ export class RoutePreflight {
36
+ private readonly ctx: Context
37
+ private readonly onDegrade: (key: string, note: string) => void
38
+ private readonly capabilities = new Map<string, Promise<Capability>>()
39
+ private readonly degraded = new Set<string>()
40
+
41
+ /** @param options - the plugin context and the degrade reporter. */
42
+ constructor(options: RoutePreflightOptions) {
43
+ this.ctx = options.ctx
44
+ this.onDegrade = options.onDegrade
45
+ }
46
+
47
+ /** One cached capability lookup; a failing adapter is treated as lenient. */
48
+ private capability(provider: string, model: string): Promise<Capability> {
49
+ const key = `${provider}/${model}`
50
+ let pending = this.capabilities.get(key)
51
+ if (pending === undefined) {
52
+ const providers = this.ctx.llm.listProviders()
53
+ const registered = providers.some(entry => entry.id === provider)
54
+ pending = registered
55
+ ? this.ctx.llm.resolveModelInfo(provider, model).then(
56
+ info => ({ registered: true, efforts: info.reasoning === undefined ? undefined : new Set(info.reasoning.efforts.map(effort => String(effort.id))) }),
57
+ () => ({ registered: true, efforts: undefined }),
58
+ )
59
+ : Promise.resolve({ registered: false, efforts: undefined })
60
+ this.capabilities.set(key, pending)
61
+ }
62
+ return pending
63
+ }
64
+
65
+ /** Report one degradation at most once per key. */
66
+ private degrade(key: string, note: string): void {
67
+ if (this.degraded.has(key)) return
68
+ this.degraded.add(key)
69
+ this.onDegrade(key, note)
70
+ }
71
+
72
+ /**
73
+ * Make one tier landing safe to apply.
74
+ * @param route - the landing the policy produced.
75
+ * @param tier - the tier configuration, for its fallback chain.
76
+ * @returns a landing that will not fail the request on provider/effort facts.
77
+ */
78
+ async sanitize(route: TierRoute, tier: ResolvedTierConfig): Promise<TierRoute> {
79
+ const capability = await this.capability(route.provider, route.model)
80
+ let safe = route
81
+ if (!capability.registered) {
82
+ const entry = tier.fallback.find(candidate => this.ctx.llm.listProviders().some(p => p.id === candidate.provider))
83
+ if (entry !== undefined) {
84
+ this.degrade(
85
+ `provider:${route.provider}`,
86
+ `provider "${route.provider}" is not registered; using fallback "${entry.provider}/${entry.model}"`,
87
+ )
88
+ safe = tier.followSession
89
+ ? { provider: entry.provider, model: entry.model }
90
+ : { provider: entry.provider, model: entry.model, effort: tier.effort }
91
+ } else {
92
+ this.degrade(`provider:${route.provider}`, `provider "${route.provider}" is not registered and the tier has no usable fallback`)
93
+ }
94
+ }
95
+ const effort = safe.effort
96
+ if (effort !== undefined && capability.efforts !== undefined && !capability.efforts.has(effort)) {
97
+ this.degrade(
98
+ `effort:${route.provider}/${route.model}`,
99
+ `model "${route.model}" does not accept reasoning effort "${effort}"; the adapter default applies`,
100
+ )
101
+ safe = { provider: safe.provider, model: safe.model }
102
+ }
103
+ return safe
104
+ }
105
+ }
package/src/routing.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  type Decision,
34
34
  type RouteState,
35
35
  } from './policy.ts'
36
+ import { RoutePreflight } from './preflight.ts'
36
37
  import type { AutotierService } from './service.ts'
37
38
  import type { AgentStateStore } from './state.ts'
38
39
  import { classifyFallback, EFFORT_LADDER, effortRank, escalationLadder, resolveRoute } from './tiers.ts'
@@ -122,6 +123,7 @@ export class AutotierRouter {
122
123
  private readonly counters = new WeakMap<Session, { toolNames: string[]; messageCount: number }>()
123
124
  /** Router-owned lifetime signal: aborts in-flight judge calls on unload. */
124
125
  private readonly lifetime = new AbortController()
126
+ private readonly preflight: RoutePreflight
125
127
  private disposed = false
126
128
 
127
129
  /**
@@ -132,6 +134,10 @@ export class AutotierRouter {
132
134
  this.ctx = options.ctx
133
135
  this.service = options.service
134
136
  this.states = options.states
137
+ this.preflight = new RoutePreflight({
138
+ ctx: this.ctx,
139
+ onDegrade: (_key, note) => this.ctx.logger.warn('dsh-autotier: %s', note),
140
+ })
135
141
  this.ctx.on('agent/inbox/inserted', payload => this.onInboxInserted(payload.agent, payload.message), { prepend: true })
136
142
  this.ctx.on('agent/request', (payload, next) => this.onRequest(payload.agent, payload.turn, payload.step, next), { prepend: true })
137
143
  this.ctx.on('agent/error', payload => this.onAgentError(payload.agent, payload.error))
@@ -431,7 +437,11 @@ export class AutotierRouter {
431
437
  const source: RouteSource = veto === undefined || veto === null ? decision.source : 'manual'
432
438
  const reason = veto === undefined || veto === null ? decision.reason : `veto: ${veto.reason}`
433
439
  const route = this.routeFor(tier, config, intent, state, now, base)
434
- const applied = resolveRoute(base, route)
440
+ // Pre-flight the landing against the live registry: an unregistered
441
+ // provider or an effort the target model rejects degrades here instead of
442
+ // failing the request.
443
+ const safeRoute = await this.preflight.sanitize(route, tier === 'strong' ? config.tiers.strong : config.tiers.cheap)
444
+ const applied = resolveRoute(base, safeRoute)
435
445
  if (state.appliedTier !== tier) {
436
446
  const from = state.appliedTier
437
447
  state.appliedTier = tier