Skip to content

Commit d76c78a

Browse files
author
谁在吵着吃糖
committed
fix(plugin): resolve auxiliary reasoning capabilities
1 parent b90b5c9 commit d76c78a

4 files changed

Lines changed: 370 additions & 16 deletions

File tree

‎apps/memos-local-plugin/adapters/deepseek-harness/host-llm.ts‎

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type GenerateOptions,
1111
type LlmCallConfig,
1212
type LlmFailure,
13+
type LlmResolvedModelInfo,
1314
type PreparedLlmCall,
1415
type StreamChunk,
1516
} from "@deepseek-ai/dsh-llm";
@@ -29,6 +30,7 @@ const HOST_LLM_TIMEOUT_CODE = "MEMOS_DSH_HOST_LLM_TIMEOUT";
2930
const HOST_LLM_MESSAGE_SOURCE = "memos-local-memory";
3031
const NO_REASONING_EFFORT = ReasoningEffortId("off");
3132
const UNSUPPORTED_REASONING_EFFORT = "UNSUPPORTED_REASONING_EFFORT";
33+
const MODEL_CAPABILITY_TTL_MS = 10 * 60 * 1_000;
3234

3335
/** Atomic provider/model route captured from the DSH agent that owns a turn. */
3436
export interface DeepSeekHarnessLlmRoute {
@@ -40,12 +42,34 @@ export interface DeepSeekHarnessLlmRoute {
4042

4143
/** Public subset of DSH's LLM runtime used by this adapter. */
4244
export interface DeepSeekHarnessLlmLike {
45+
resolveModelInfo(
46+
provider: string,
47+
model: string,
48+
signal?: AbortSignal,
49+
): Promise<LlmResolvedModelInfo>;
4350
prepareCall(
4451
config: LlmCallConfig,
4552
signal?: AbortSignal,
4653
): Promise<PreparedLlmCall>;
4754
}
4855

56+
export interface DeepSeekHarnessHostLlmBridge extends HostLlmBridge {
57+
/** Drop exact-route capability snapshots after a DSH adapter topology update. */
58+
invalidateModelCapabilities(): void;
59+
}
60+
61+
type AuxiliaryReasoningCapability = "off" | "plain";
62+
63+
interface CapabilityCacheEntry {
64+
readonly expiresAt: number;
65+
readonly value: Promise<AuxiliaryReasoningCapability>;
66+
}
67+
68+
interface CapabilityCache {
69+
readonly entries: Map<string, CapabilityCacheEntry>;
70+
generation: number;
71+
}
72+
4973
/**
5074
* Async route scope for MemOS work spawned by one DSH session.
5175
*
@@ -95,9 +119,17 @@ export interface CreateDeepSeekHarnessHostLlmBridgeOptions {
95119
*/
96120
export function createDeepSeekHarnessHostLlmBridge(
97121
options: CreateDeepSeekHarnessHostLlmBridgeOptions,
98-
): HostLlmBridge {
122+
): DeepSeekHarnessHostLlmBridge {
123+
const capabilityCache: CapabilityCache = {
124+
entries: new Map(),
125+
generation: 0,
126+
};
99127
return {
100128
id: HOST_LLM_BRIDGE_ID,
129+
invalidateModelCapabilities(): void {
130+
capabilityCache.generation++;
131+
capabilityCache.entries.clear();
132+
},
101133
async complete(input: HostLlmCompleteInput): Promise<HostLlmCompletion> {
102134
const route = options.routes.current();
103135
if (!route) {
@@ -122,6 +154,7 @@ export function createDeepSeekHarnessHostLlmBridge(
122154
input,
123155
route,
124156
callDeadline.signal,
157+
capabilityCache,
125158
);
126159
const request = createGenerateOptions(input, route, prepared.config);
127160
request.signal = callDeadline.signal;
@@ -217,20 +250,30 @@ function createGenerateOptions(
217250
*
218251
* Retrieval filters and JSON extractors intentionally use small output caps.
219252
* Reusing a conversation's high reasoning effort can spend that entire cap on
220-
* reasoning and produce no JSON/text. DSH effort ids are adapter-owned, so the
221-
* exact registered adapter validates the branded conventional `off` id. Only
222-
* an explicit unsupported-effort result retries without it, preserving the
223-
* adapter/provider default. prepareCall performs no provider generation I/O
224-
* and binds that validation to the same registration used for dispatch, even
225-
* if HMR replaces the route before the returned stream starts.
253+
* reasoning and produce no JSON/text. Resolve exact-model metadata through
254+
* DSH, then cache whether its adapter advertises the conventional `off` id.
255+
* prepareCall remains the final registration-bound authority: if HMR changes
256+
* the route after metadata resolution, an explicit unsupported-effort result
257+
* refreshes the cache and retries with the adapter/provider default.
226258
*/
227259
async function prepareAuxiliaryCall(
228260
llm: DeepSeekHarnessLlmLike,
229261
input: HostLlmCompleteInput,
230262
route: DeepSeekHarnessLlmRoute,
231263
signal: AbortSignal,
264+
capabilityCache: CapabilityCache,
232265
): Promise<PreparedLlmCall> {
233266
const config = createCallConfig(input, route);
267+
const capability = await resolveAuxiliaryReasoningCapability(
268+
llm,
269+
route,
270+
signal,
271+
capabilityCache,
272+
);
273+
if (capability === "plain") {
274+
return llm.prepareCall(config, signal);
275+
}
276+
const preparationGeneration = capabilityCache.generation;
234277
try {
235278
return await llm.prepareCall(
236279
{ ...config, reasoningEffort: NO_REASONING_EFFORT },
@@ -243,10 +286,64 @@ async function prepareAuxiliaryCall(
243286
) {
244287
throw error;
245288
}
289+
// The exact adapter may have changed after the metadata lookup. Preserve
290+
// prepareCall's registration-bound validation as the final authority and
291+
// remember the corrected capability only if no newer topology update has
292+
// already invalidated this preparation generation.
293+
if (capabilityCache.generation === preparationGeneration) {
294+
rememberAuxiliaryReasoningCapability(route, "plain", capabilityCache);
295+
}
246296
return llm.prepareCall(config, signal);
247297
}
248298
}
249299

300+
function capabilityRouteKey(route: DeepSeekHarnessLlmRoute): string {
301+
return JSON.stringify([route.provider, route.model]);
302+
}
303+
304+
function rememberAuxiliaryReasoningCapability(
305+
route: DeepSeekHarnessLlmRoute,
306+
capability: AuxiliaryReasoningCapability,
307+
cache: CapabilityCache,
308+
): void {
309+
cache.entries.set(capabilityRouteKey(route), {
310+
expiresAt: Date.now() + MODEL_CAPABILITY_TTL_MS,
311+
value: Promise.resolve(capability),
312+
});
313+
}
314+
315+
async function resolveAuxiliaryReasoningCapability(
316+
llm: DeepSeekHarnessLlmLike,
317+
route: DeepSeekHarnessLlmRoute,
318+
signal: AbortSignal,
319+
cache: CapabilityCache,
320+
): Promise<AuxiliaryReasoningCapability> {
321+
const key = capabilityRouteKey(route);
322+
const cached = cache.entries.get(key);
323+
if (cached && cached.expiresAt > Date.now()) {
324+
return cached.value;
325+
}
326+
if (cached) cache.entries.delete(key);
327+
328+
let entry: CapabilityCacheEntry;
329+
const value = llm.resolveModelInfo(route.provider, route.model, signal)
330+
.then((info): AuxiliaryReasoningCapability => (
331+
info.reasoning?.efforts.some((effort) => effort.id === NO_REASONING_EFFORT)
332+
? "off"
333+
: "plain"
334+
))
335+
.catch((error: unknown) => {
336+
if (cache.entries.get(key) === entry) cache.entries.delete(key);
337+
throw error;
338+
});
339+
entry = {
340+
expiresAt: Date.now() + MODEL_CAPABILITY_TTL_MS,
341+
value,
342+
};
343+
cache.entries.set(key, entry);
344+
return value;
345+
}
346+
250347
function createCallConfig(
251348
input: HostLlmCompleteInput,
252349
route: DeepSeekHarnessLlmRoute,

‎apps/memos-local-plugin/adapters/deepseek-harness/index.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
import {
3737
createDeepSeekHarnessHostLlmBridge,
3838
DeepSeekHarnessLlmRouteContext,
39+
type DeepSeekHarnessHostLlmBridge,
3940
} from "./host-llm.js";
4041
import { registerDeepSeekHarnessTools } from "./tools.js";
4142

@@ -126,6 +127,16 @@ export function configureDeepSeekHarnessHostLlm(
126127
});
127128
}
128129

130+
/** Refresh exact-model capability snapshots whenever DSH replaces an adapter. */
131+
export function registerDeepSeekHarnessHostLlmCapabilityInvalidation(
132+
ctx: Context,
133+
bridge: DeepSeekHarnessHostLlmBridge,
134+
): () => void {
135+
return ctx.on("llm/adapters-updated", () => {
136+
bridge.invalidateModelCapabilities();
137+
});
138+
}
139+
129140
/** Autonomous recovery has no owning DSH turn from which to capture a route. */
130141
export function deepSeekHarnessAutoRecoveryEnabled(config: ResolvedConfig): boolean {
131142
return config.llm.provider.trim().toLowerCase() !== "host" ||
@@ -247,6 +258,12 @@ export async function apply(
247258
const hostLlmBridge = config.hostLlmEnabled
248259
? createDeepSeekHarnessHostLlmBridge({ llm: ctx.llm, routes })
249260
: null;
261+
if (hostLlmBridge) {
262+
registrations.push(registerDeepSeekHarnessHostLlmCapabilityInvalidation(
263+
ctx,
264+
hostLlmBridge,
265+
));
266+
}
250267
const autoRecoveryEnabled = deepSeekHarnessAutoRecoveryEnabled(memoryConfig);
251268

252269
core = await bootstrapMemoryCore({

0 commit comments

Comments
 (0)