@martian-engineering/lossless-claw 0.2.6 → 0.2.7
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/index.ts +109 -352
- package/package.json +4 -1
- package/src/summarize.ts +3 -2
- package/src/types.ts +16 -2
package/index.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* DAG-based conversation summarization with incremental compaction,
|
|
5
5
|
* full-text search, and sub-agent expansion.
|
|
6
6
|
*/
|
|
7
|
-
import { readFileSync
|
|
8
|
-
import { join } from "node:path";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
9
8
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
10
9
|
import { resolveLcmConfig } from "./src/db/config.js";
|
|
11
10
|
import { LcmContextEngine } from "./src/engine.js";
|
|
@@ -46,12 +45,8 @@ type PluginEnvSnapshot = {
|
|
|
46
45
|
pluginSummaryProvider: string;
|
|
47
46
|
openclawProvider: string;
|
|
48
47
|
openclawDefaultModel: string;
|
|
49
|
-
agentDir: string;
|
|
50
|
-
home: string;
|
|
51
48
|
};
|
|
52
49
|
|
|
53
|
-
type ReadEnvFn = (key: string) => string | undefined;
|
|
54
|
-
|
|
55
50
|
type CompleteSimpleOptions = {
|
|
56
51
|
apiKey?: string;
|
|
57
52
|
maxTokens: number;
|
|
@@ -59,6 +54,42 @@ type CompleteSimpleOptions = {
|
|
|
59
54
|
reasoning?: string;
|
|
60
55
|
};
|
|
61
56
|
|
|
57
|
+
type RuntimeModelAuthResult = {
|
|
58
|
+
apiKey?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type RuntimeModelAuthModel = {
|
|
62
|
+
id: string;
|
|
63
|
+
provider: string;
|
|
64
|
+
api: string;
|
|
65
|
+
name?: string;
|
|
66
|
+
reasoning?: boolean;
|
|
67
|
+
input?: string[];
|
|
68
|
+
cost?: {
|
|
69
|
+
input: number;
|
|
70
|
+
output: number;
|
|
71
|
+
cacheRead: number;
|
|
72
|
+
cacheWrite: number;
|
|
73
|
+
};
|
|
74
|
+
contextWindow?: number;
|
|
75
|
+
maxTokens?: number;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type RuntimeModelAuth = {
|
|
79
|
+
getApiKeyForModel: (params: {
|
|
80
|
+
model: RuntimeModelAuthModel;
|
|
81
|
+
cfg?: OpenClawPluginApi["config"];
|
|
82
|
+
profileId?: string;
|
|
83
|
+
preferredProfile?: string;
|
|
84
|
+
}) => Promise<RuntimeModelAuthResult | undefined>;
|
|
85
|
+
resolveApiKeyForProvider: (params: {
|
|
86
|
+
provider: string;
|
|
87
|
+
cfg?: OpenClawPluginApi["config"];
|
|
88
|
+
profileId?: string;
|
|
89
|
+
preferredProfile?: string;
|
|
90
|
+
}) => Promise<RuntimeModelAuthResult | undefined>;
|
|
91
|
+
};
|
|
92
|
+
|
|
62
93
|
/** Capture plugin env values once during initialization. */
|
|
63
94
|
function snapshotPluginEnv(env: NodeJS.ProcessEnv = process.env): PluginEnvSnapshot {
|
|
64
95
|
return {
|
|
@@ -68,8 +99,6 @@ function snapshotPluginEnv(env: NodeJS.ProcessEnv = process.env): PluginEnvSnaps
|
|
|
68
99
|
pluginSummaryProvider: "",
|
|
69
100
|
openclawProvider: env.OPENCLAW_PROVIDER?.trim() ?? "",
|
|
70
101
|
openclawDefaultModel: "",
|
|
71
|
-
agentDir: env.OPENCLAW_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || "",
|
|
72
|
-
home: env.HOME?.trim() ?? "",
|
|
73
102
|
};
|
|
74
103
|
}
|
|
75
104
|
|
|
@@ -88,58 +117,6 @@ function readDefaultModelFromConfig(config: unknown): string {
|
|
|
88
117
|
return typeof primary === "string" ? primary.trim() : "";
|
|
89
118
|
}
|
|
90
119
|
|
|
91
|
-
/** Resolve common provider API keys from environment. */
|
|
92
|
-
function resolveApiKey(provider: string, readEnv: ReadEnvFn): string | undefined {
|
|
93
|
-
const keyMap: Record<string, string[]> = {
|
|
94
|
-
openai: ["OPENAI_API_KEY"],
|
|
95
|
-
anthropic: ["ANTHROPIC_API_KEY"],
|
|
96
|
-
google: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
|
97
|
-
groq: ["GROQ_API_KEY"],
|
|
98
|
-
xai: ["XAI_API_KEY"],
|
|
99
|
-
mistral: ["MISTRAL_API_KEY"],
|
|
100
|
-
together: ["TOGETHER_API_KEY"],
|
|
101
|
-
openrouter: ["OPENROUTER_API_KEY"],
|
|
102
|
-
"github-copilot": ["GITHUB_COPILOT_API_KEY", "GITHUB_TOKEN"],
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const providerKey = provider.trim().toLowerCase();
|
|
106
|
-
const keys = keyMap[providerKey] ?? [];
|
|
107
|
-
const normalizedProviderEnv = `${providerKey.replace(/[^a-z0-9]/g, "_").toUpperCase()}_API_KEY`;
|
|
108
|
-
keys.push(normalizedProviderEnv);
|
|
109
|
-
|
|
110
|
-
for (const key of keys) {
|
|
111
|
-
const value = readEnv(key)?.trim();
|
|
112
|
-
if (value) {
|
|
113
|
-
return value;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
type AuthProfileCredential =
|
|
120
|
-
| { type: "api_key"; provider: string; key?: string; email?: string }
|
|
121
|
-
| { type: "token"; provider: string; token?: string; expires?: number; email?: string }
|
|
122
|
-
| ({
|
|
123
|
-
type: "oauth";
|
|
124
|
-
provider: string;
|
|
125
|
-
access?: string;
|
|
126
|
-
refresh?: string;
|
|
127
|
-
expires?: number;
|
|
128
|
-
email?: string;
|
|
129
|
-
} & Record<string, unknown>);
|
|
130
|
-
|
|
131
|
-
type AuthProfileStore = {
|
|
132
|
-
profiles: Record<string, AuthProfileCredential>;
|
|
133
|
-
order?: Record<string, string[]>;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
type PiAiOAuthCredentials = {
|
|
137
|
-
refresh: string;
|
|
138
|
-
access: string;
|
|
139
|
-
expires: number;
|
|
140
|
-
[key: string]: unknown;
|
|
141
|
-
};
|
|
142
|
-
|
|
143
120
|
type PiAiModule = {
|
|
144
121
|
completeSimple?: (
|
|
145
122
|
model: {
|
|
@@ -171,11 +148,6 @@ type PiAiModule = {
|
|
|
171
148
|
) => Promise<Record<string, unknown> & { content?: Array<{ type: string; text?: string }> }>;
|
|
172
149
|
getModel?: (provider: string, modelId: string) => unknown;
|
|
173
150
|
getModels?: (provider: string) => unknown[];
|
|
174
|
-
getEnvApiKey?: (provider: string) => string | undefined;
|
|
175
|
-
getOAuthApiKey?: (
|
|
176
|
-
providerId: string,
|
|
177
|
-
credentials: Record<string, PiAiOAuthCredentials>,
|
|
178
|
-
) => Promise<{ apiKey: string; newCredentials: PiAiOAuthCredentials } | null>;
|
|
179
151
|
};
|
|
180
152
|
|
|
181
153
|
/** Narrow unknown values to plain objects. */
|
|
@@ -279,283 +251,45 @@ function resolveProviderApiFromRuntimeConfig(
|
|
|
279
251
|
return typeof api === "string" && api.trim() ? api.trim() : undefined;
|
|
280
252
|
}
|
|
281
253
|
|
|
282
|
-
/**
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (!isRecord(parsed) || !isRecord(parsed.profiles)) {
|
|
287
|
-
return undefined;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const profiles: Record<string, AuthProfileCredential> = {};
|
|
291
|
-
for (const [profileId, value] of Object.entries(parsed.profiles)) {
|
|
292
|
-
if (!isRecord(value)) {
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
const type = value.type;
|
|
296
|
-
const provider = typeof value.provider === "string" ? value.provider.trim() : "";
|
|
297
|
-
if (!provider || (type !== "api_key" && type !== "token" && type !== "oauth")) {
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
profiles[profileId] = value as AuthProfileCredential;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const rawOrder = isRecord(parsed.order) ? parsed.order : undefined;
|
|
304
|
-
const order: Record<string, string[]> | undefined = rawOrder
|
|
305
|
-
? Object.entries(rawOrder).reduce<Record<string, string[]>>((acc, [provider, value]) => {
|
|
306
|
-
if (!Array.isArray(value)) {
|
|
307
|
-
return acc;
|
|
308
|
-
}
|
|
309
|
-
const ids = value
|
|
310
|
-
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
|
311
|
-
.filter(Boolean);
|
|
312
|
-
if (ids.length > 0) {
|
|
313
|
-
acc[provider] = ids;
|
|
314
|
-
}
|
|
315
|
-
return acc;
|
|
316
|
-
}, {})
|
|
317
|
-
: undefined;
|
|
318
|
-
|
|
319
|
-
return {
|
|
320
|
-
profiles,
|
|
321
|
-
...(order && Object.keys(order).length > 0 ? { order } : {}),
|
|
322
|
-
};
|
|
323
|
-
} catch {
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** Merge auth stores, letting later stores override earlier profiles/order. */
|
|
329
|
-
function mergeAuthProfileStores(stores: AuthProfileStore[]): AuthProfileStore | undefined {
|
|
330
|
-
if (stores.length === 0) {
|
|
331
|
-
return undefined;
|
|
332
|
-
}
|
|
333
|
-
const merged: AuthProfileStore = { profiles: {} };
|
|
334
|
-
for (const store of stores) {
|
|
335
|
-
merged.profiles = { ...merged.profiles, ...store.profiles };
|
|
336
|
-
if (store.order) {
|
|
337
|
-
merged.order = { ...(merged.order ?? {}), ...store.order };
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return merged;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** Determine candidate auth store paths ordered by precedence. */
|
|
344
|
-
function resolveAuthStorePaths(params: { agentDir?: string; envSnapshot: PluginEnvSnapshot }): string[] {
|
|
345
|
-
const paths: string[] = [];
|
|
346
|
-
const directAgentDir = params.agentDir?.trim();
|
|
347
|
-
if (directAgentDir) {
|
|
348
|
-
paths.push(join(directAgentDir, "auth-profiles.json"));
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const envAgentDir = params.envSnapshot.agentDir;
|
|
352
|
-
if (envAgentDir) {
|
|
353
|
-
paths.push(join(envAgentDir, "auth-profiles.json"));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const home = params.envSnapshot.home;
|
|
357
|
-
if (home) {
|
|
358
|
-
paths.push(join(home, ".openclaw", "agents", "main", "agent", "auth-profiles.json"));
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return [...new Set(paths)];
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/** Build profile selection order for provider auth lookup. */
|
|
365
|
-
function resolveAuthProfileCandidates(params: {
|
|
366
|
-
provider: string;
|
|
367
|
-
store: AuthProfileStore;
|
|
368
|
-
authProfileId?: string;
|
|
369
|
-
runtimeConfig?: unknown;
|
|
370
|
-
}): string[] {
|
|
371
|
-
const candidates: string[] = [];
|
|
372
|
-
const normalizedProvider = normalizeProviderId(params.provider);
|
|
373
|
-
const push = (value: string | undefined) => {
|
|
374
|
-
const profileId = value?.trim();
|
|
375
|
-
if (!profileId) {
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
if (!candidates.includes(profileId)) {
|
|
379
|
-
candidates.push(profileId);
|
|
380
|
-
}
|
|
254
|
+
/** Resolve runtime.modelAuth from plugin runtime, even before plugin-sdk typings land locally. */
|
|
255
|
+
function getRuntimeModelAuth(api: OpenClawPluginApi): RuntimeModelAuth {
|
|
256
|
+
const runtime = api.runtime as OpenClawPluginApi["runtime"] & {
|
|
257
|
+
modelAuth?: RuntimeModelAuth;
|
|
381
258
|
};
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const storeOrder = findProviderConfigValue(params.store.order, params.provider);
|
|
386
|
-
for (const profileId of storeOrder ?? []) {
|
|
387
|
-
push(profileId);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (isRecord(params.runtimeConfig)) {
|
|
391
|
-
const auth = params.runtimeConfig.auth;
|
|
392
|
-
if (isRecord(auth)) {
|
|
393
|
-
const order = findProviderConfigValue(
|
|
394
|
-
isRecord(auth.order) ? (auth.order as Record<string, unknown>) : undefined,
|
|
395
|
-
params.provider,
|
|
396
|
-
);
|
|
397
|
-
if (Array.isArray(order)) {
|
|
398
|
-
for (const profileId of order) {
|
|
399
|
-
if (typeof profileId === "string") {
|
|
400
|
-
push(profileId);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
for (const [profileId, credential] of Object.entries(params.store.profiles)) {
|
|
408
|
-
if (normalizeProviderId(credential.provider) === normalizedProvider) {
|
|
409
|
-
push(profileId);
|
|
410
|
-
}
|
|
259
|
+
if (!runtime.modelAuth) {
|
|
260
|
+
throw new Error("OpenClaw runtime.modelAuth is required by lossless-claw.");
|
|
411
261
|
}
|
|
412
|
-
|
|
413
|
-
return candidates;
|
|
262
|
+
return runtime.modelAuth;
|
|
414
263
|
}
|
|
415
264
|
|
|
416
|
-
/**
|
|
417
|
-
|
|
265
|
+
/** Build the minimal model shape required by runtime.modelAuth.getApiKeyForModel(). */
|
|
266
|
+
function buildModelAuthLookupModel(params: {
|
|
418
267
|
provider: string;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const storesWithPaths = resolveAuthStorePaths({
|
|
426
|
-
agentDir: params.agentDir,
|
|
427
|
-
envSnapshot: params.envSnapshot,
|
|
428
|
-
})
|
|
429
|
-
.map((path) => {
|
|
430
|
-
try {
|
|
431
|
-
const parsed = parseAuthProfileStore(readFileSync(path, "utf8"));
|
|
432
|
-
return parsed ? { path, store: parsed } : undefined;
|
|
433
|
-
} catch {
|
|
434
|
-
return undefined;
|
|
435
|
-
}
|
|
436
|
-
})
|
|
437
|
-
.filter((entry): entry is { path: string; store: AuthProfileStore } => !!entry);
|
|
438
|
-
if (storesWithPaths.length === 0) {
|
|
439
|
-
return undefined;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const mergedStore = mergeAuthProfileStores(storesWithPaths.map((entry) => entry.store));
|
|
443
|
-
if (!mergedStore) {
|
|
444
|
-
return undefined;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const candidates = resolveAuthProfileCandidates({
|
|
268
|
+
model: string;
|
|
269
|
+
api?: string;
|
|
270
|
+
}): RuntimeModelAuthModel {
|
|
271
|
+
return {
|
|
272
|
+
id: params.model,
|
|
273
|
+
name: params.model,
|
|
448
274
|
provider: params.provider,
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (!credential) {
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
if (normalizeProviderId(credential.provider) !== normalizeProviderId(params.provider)) {
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (credential.type === "api_key") {
|
|
470
|
-
const key = credential.key?.trim();
|
|
471
|
-
if (key) {
|
|
472
|
-
return key;
|
|
473
|
-
}
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (credential.type === "token") {
|
|
478
|
-
const token = credential.token?.trim();
|
|
479
|
-
if (!token) {
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
const expires = credential.expires;
|
|
483
|
-
if (typeof expires === "number" && Number.isFinite(expires) && expires > 0 && Date.now() >= expires) {
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
return token;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const access = credential.access?.trim();
|
|
490
|
-
const expires = credential.expires;
|
|
491
|
-
const isExpired =
|
|
492
|
-
typeof expires === "number" && Number.isFinite(expires) && expires > 0 && Date.now() >= expires;
|
|
493
|
-
|
|
494
|
-
if (!isExpired && access) {
|
|
495
|
-
if (
|
|
496
|
-
(credential.provider === "google-gemini-cli" || credential.provider === "google-antigravity") &&
|
|
497
|
-
typeof credential.projectId === "string" &&
|
|
498
|
-
credential.projectId.trim()
|
|
499
|
-
) {
|
|
500
|
-
return JSON.stringify({
|
|
501
|
-
token: access,
|
|
502
|
-
projectId: credential.projectId.trim(),
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
return access;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (typeof params.piAiModule.getOAuthApiKey !== "function") {
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
try {
|
|
513
|
-
const oauthCredential = {
|
|
514
|
-
access: credential.access ?? "",
|
|
515
|
-
refresh: credential.refresh ?? "",
|
|
516
|
-
expires: typeof credential.expires === "number" ? credential.expires : 0,
|
|
517
|
-
...(typeof credential.projectId === "string" ? { projectId: credential.projectId } : {}),
|
|
518
|
-
...(typeof credential.accountId === "string" ? { accountId: credential.accountId } : {}),
|
|
519
|
-
};
|
|
520
|
-
const refreshed = await params.piAiModule.getOAuthApiKey(params.provider, {
|
|
521
|
-
[params.provider]: oauthCredential,
|
|
522
|
-
});
|
|
523
|
-
if (!refreshed?.apiKey) {
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
mergedStore.profiles[profileId] = {
|
|
527
|
-
...credential,
|
|
528
|
-
...refreshed.newCredentials,
|
|
529
|
-
type: "oauth",
|
|
530
|
-
};
|
|
531
|
-
if (persistPath) {
|
|
532
|
-
try {
|
|
533
|
-
writeFileSync(
|
|
534
|
-
persistPath,
|
|
535
|
-
JSON.stringify(
|
|
536
|
-
{
|
|
537
|
-
version: 1,
|
|
538
|
-
profiles: mergedStore.profiles,
|
|
539
|
-
...(mergedStore.order ? { order: mergedStore.order } : {}),
|
|
540
|
-
},
|
|
541
|
-
null,
|
|
542
|
-
2,
|
|
543
|
-
),
|
|
544
|
-
"utf8",
|
|
545
|
-
);
|
|
546
|
-
} catch {
|
|
547
|
-
// Ignore persistence errors: refreshed credentials remain usable in-memory for this run.
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
return refreshed.apiKey;
|
|
551
|
-
} catch {
|
|
552
|
-
if (access) {
|
|
553
|
-
return access;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
275
|
+
api: params.api?.trim() || inferApiFromProvider(params.provider),
|
|
276
|
+
reasoning: false,
|
|
277
|
+
input: ["text"],
|
|
278
|
+
cost: {
|
|
279
|
+
input: 0,
|
|
280
|
+
output: 0,
|
|
281
|
+
cacheRead: 0,
|
|
282
|
+
cacheWrite: 0,
|
|
283
|
+
},
|
|
284
|
+
contextWindow: 200_000,
|
|
285
|
+
maxTokens: 8_000,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
557
288
|
|
|
558
|
-
|
|
289
|
+
/** Normalize an auth result down to the API key that pi-ai expects. */
|
|
290
|
+
function resolveApiKeyFromAuthResult(auth: RuntimeModelAuthResult | undefined): string | undefined {
|
|
291
|
+
const apiKey = auth?.apiKey?.trim();
|
|
292
|
+
return apiKey ? apiKey : undefined;
|
|
559
293
|
}
|
|
560
294
|
|
|
561
295
|
/** Build a minimal but useful sub-agent prompt. */
|
|
@@ -618,7 +352,7 @@ function readLatestAssistantReply(messages: unknown[]): string | undefined {
|
|
|
618
352
|
function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
619
353
|
const envSnapshot = snapshotPluginEnv();
|
|
620
354
|
envSnapshot.openclawDefaultModel = readDefaultModelFromConfig(api.config);
|
|
621
|
-
const
|
|
355
|
+
const modelAuth = getRuntimeModelAuth(api);
|
|
622
356
|
const pluginConfig =
|
|
623
357
|
api.pluginConfig && typeof api.pluginConfig === "object" && !Array.isArray(api.pluginConfig)
|
|
624
358
|
? api.pluginConfig
|
|
@@ -713,19 +447,22 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
713
447
|
maxTokens: 8_000,
|
|
714
448
|
};
|
|
715
449
|
|
|
716
|
-
let resolvedApiKey = apiKey?.trim()
|
|
717
|
-
if (!resolvedApiKey && typeof mod.getEnvApiKey === "function") {
|
|
718
|
-
resolvedApiKey = mod.getEnvApiKey(providerId)?.trim();
|
|
719
|
-
}
|
|
450
|
+
let resolvedApiKey = apiKey?.trim();
|
|
720
451
|
if (!resolvedApiKey) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
452
|
+
try {
|
|
453
|
+
resolvedApiKey = resolveApiKeyFromAuthResult(
|
|
454
|
+
await modelAuth.resolveApiKeyForProvider({
|
|
455
|
+
provider: providerId,
|
|
456
|
+
cfg: api.config,
|
|
457
|
+
...(authProfileId ? { profileId: authProfileId } : {}),
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.error(
|
|
462
|
+
`[lcm] modelAuth.resolveApiKeyForProvider FAILED:`,
|
|
463
|
+
err instanceof Error ? err.message : err,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
729
466
|
}
|
|
730
467
|
|
|
731
468
|
const completeOptions = buildCompleteSimpleOptions({
|
|
@@ -849,11 +586,31 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
849
586
|
).trim();
|
|
850
587
|
return { provider, model: raw };
|
|
851
588
|
},
|
|
852
|
-
getApiKey: (provider) =>
|
|
853
|
-
|
|
854
|
-
|
|
589
|
+
getApiKey: async (provider, model, options) => {
|
|
590
|
+
try {
|
|
591
|
+
return resolveApiKeyFromAuthResult(
|
|
592
|
+
await modelAuth.getApiKeyForModel({
|
|
593
|
+
model: buildModelAuthLookupModel({ provider, model }),
|
|
594
|
+
cfg: api.config,
|
|
595
|
+
...(options?.profileId ? { profileId: options.profileId } : {}),
|
|
596
|
+
...(options?.preferredProfile ? { preferredProfile: options.preferredProfile } : {}),
|
|
597
|
+
}),
|
|
598
|
+
);
|
|
599
|
+
} catch {
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
requireApiKey: async (provider, model, options) => {
|
|
604
|
+
const key = await resolveApiKeyFromAuthResult(
|
|
605
|
+
await modelAuth.getApiKeyForModel({
|
|
606
|
+
model: buildModelAuthLookupModel({ provider, model }),
|
|
607
|
+
cfg: api.config,
|
|
608
|
+
...(options?.profileId ? { profileId: options.profileId } : {}),
|
|
609
|
+
...(options?.preferredProfile ? { preferredProfile: options.preferredProfile } : {}),
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
855
612
|
if (!key) {
|
|
856
|
-
throw new Error(`Missing API key for provider '${provider}'.`);
|
|
613
|
+
throw new Error(`Missing API key for provider '${provider}' (model '${model}').`);
|
|
857
614
|
}
|
|
858
615
|
return key;
|
|
859
616
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@martian-engineering/lossless-claw",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Lossless Context Management plugin for OpenClaw — DAG-based conversation summarization with incremental compaction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"README.md",
|
|
24
24
|
"LICENSE"
|
|
25
25
|
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "vitest run --dir test"
|
|
28
|
+
},
|
|
26
29
|
"dependencies": {
|
|
27
30
|
"@mariozechner/pi-agent-core": "*",
|
|
28
31
|
"@mariozechner/pi-ai": "*",
|
package/src/summarize.ts
CHANGED
|
@@ -672,8 +672,6 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
672
672
|
: undefined;
|
|
673
673
|
const providerApi = resolveProviderApiFromLegacyConfig(params.legacyParams.config, provider);
|
|
674
674
|
|
|
675
|
-
const apiKey = params.deps.getApiKey(provider, model);
|
|
676
|
-
|
|
677
675
|
const condensedTargetTokens =
|
|
678
676
|
Number.isFinite(params.deps.config.condensedTargetTokens) &&
|
|
679
677
|
params.deps.config.condensedTargetTokens > 0
|
|
@@ -691,6 +689,9 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
691
689
|
|
|
692
690
|
const mode: SummaryMode = aggressive ? "aggressive" : "normal";
|
|
693
691
|
const isCondensed = options?.isCondensed === true;
|
|
692
|
+
const apiKey = await params.deps.getApiKey(provider, model, {
|
|
693
|
+
profileId: authProfileId,
|
|
694
|
+
});
|
|
694
695
|
const targetTokens = resolveTargetTokens({
|
|
695
696
|
inputTokens: estimateTokens(text),
|
|
696
697
|
mode,
|
package/src/types.ts
CHANGED
|
@@ -58,8 +58,22 @@ export type ResolveModelFn = (modelRef?: string, providerHint?: string) => {
|
|
|
58
58
|
/**
|
|
59
59
|
* API key resolution function.
|
|
60
60
|
*/
|
|
61
|
-
export type
|
|
62
|
-
|
|
61
|
+
export type ApiKeyLookupOptions = {
|
|
62
|
+
profileId?: string;
|
|
63
|
+
preferredProfile?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type GetApiKeyFn = (
|
|
67
|
+
provider: string,
|
|
68
|
+
model: string,
|
|
69
|
+
options?: ApiKeyLookupOptions,
|
|
70
|
+
) => Promise<string | undefined>;
|
|
71
|
+
|
|
72
|
+
export type RequireApiKeyFn = (
|
|
73
|
+
provider: string,
|
|
74
|
+
model: string,
|
|
75
|
+
options?: ApiKeyLookupOptions,
|
|
76
|
+
) => Promise<string>;
|
|
63
77
|
|
|
64
78
|
/**
|
|
65
79
|
* Session key utilities.
|