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 +26 -0
- package/README.es.md +6 -2
- package/README.hi.md +7 -2
- package/README.md +7 -2
- package/README.pt.md +6 -2
- package/README.zh.md +6 -2
- package/lib/index.js +82 -1
- package/lib/types/preflight.d.ts +48 -0
- package/lib/types/preflight.d.ts.map +1 -0
- package/lib/types/routing.d.ts +1 -0
- package/lib/types/routing.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/preflight.ts +105 -0
- package/src/routing.ts +11 -1
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
|
|
23
|
-
| `0.1.5-alpha.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` |
|
|
21
|
-
| `0.1.5-alpha.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
|
|
21
|
-
| `0.1.5-alpha.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
|
|
22
|
-
| `0.1.5-alpha.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` |
|
|
18
|
-
| `0.1.5-alpha.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"}
|
package/lib/types/routing.d.ts
CHANGED
|
@@ -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":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;
|
|
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.
|
|
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": {
|
package/src/preflight.ts
ADDED
|
@@ -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
|
-
|
|
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
|