@martian-engineering/lossless-claw 0.2.6 → 0.2.8
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 +186 -7
- package/package.json +4 -1
- package/src/summarize.ts +3 -2
- package/src/types.ts +16 -2
package/index.ts
CHANGED
|
@@ -59,6 +59,46 @@ type CompleteSimpleOptions = {
|
|
|
59
59
|
reasoning?: string;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
type RuntimeModelAuthResult = {
|
|
63
|
+
apiKey?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type RuntimeModelAuthModel = {
|
|
67
|
+
id: string;
|
|
68
|
+
provider: string;
|
|
69
|
+
api: string;
|
|
70
|
+
name?: string;
|
|
71
|
+
reasoning?: boolean;
|
|
72
|
+
input?: string[];
|
|
73
|
+
cost?: {
|
|
74
|
+
input: number;
|
|
75
|
+
output: number;
|
|
76
|
+
cacheRead: number;
|
|
77
|
+
cacheWrite: number;
|
|
78
|
+
};
|
|
79
|
+
contextWindow?: number;
|
|
80
|
+
maxTokens?: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type RuntimeModelAuth = {
|
|
84
|
+
getApiKeyForModel: (params: {
|
|
85
|
+
model: RuntimeModelAuthModel;
|
|
86
|
+
cfg?: OpenClawPluginApi["config"];
|
|
87
|
+
profileId?: string;
|
|
88
|
+
preferredProfile?: string;
|
|
89
|
+
}) => Promise<RuntimeModelAuthResult | undefined>;
|
|
90
|
+
resolveApiKeyForProvider: (params: {
|
|
91
|
+
provider: string;
|
|
92
|
+
cfg?: OpenClawPluginApi["config"];
|
|
93
|
+
profileId?: string;
|
|
94
|
+
preferredProfile?: string;
|
|
95
|
+
}) => Promise<RuntimeModelAuthResult | undefined>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const MODEL_AUTH_PR_URL = "https://github.com/openclaw/openclaw/pull/41090";
|
|
99
|
+
const MODEL_AUTH_MERGE_COMMIT = "4790e40";
|
|
100
|
+
const MODEL_AUTH_REQUIRED_RELEASE = "the first OpenClaw release after 2026.3.8";
|
|
101
|
+
|
|
62
102
|
/** Capture plugin env values once during initialization. */
|
|
63
103
|
function snapshotPluginEnv(env: NodeJS.ProcessEnv = process.env): PluginEnvSnapshot {
|
|
64
104
|
return {
|
|
@@ -279,6 +319,53 @@ function resolveProviderApiFromRuntimeConfig(
|
|
|
279
319
|
return typeof api === "string" && api.trim() ? api.trim() : undefined;
|
|
280
320
|
}
|
|
281
321
|
|
|
322
|
+
/** Resolve runtime.modelAuth from plugin runtime when available. */
|
|
323
|
+
function getRuntimeModelAuth(api: OpenClawPluginApi): RuntimeModelAuth | undefined {
|
|
324
|
+
const runtime = api.runtime as OpenClawPluginApi["runtime"] & {
|
|
325
|
+
modelAuth?: RuntimeModelAuth;
|
|
326
|
+
};
|
|
327
|
+
return runtime.modelAuth;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Build the minimal model shape required by runtime.modelAuth.getApiKeyForModel(). */
|
|
331
|
+
function buildModelAuthLookupModel(params: {
|
|
332
|
+
provider: string;
|
|
333
|
+
model: string;
|
|
334
|
+
api?: string;
|
|
335
|
+
}): RuntimeModelAuthModel {
|
|
336
|
+
return {
|
|
337
|
+
id: params.model,
|
|
338
|
+
name: params.model,
|
|
339
|
+
provider: params.provider,
|
|
340
|
+
api: params.api?.trim() || inferApiFromProvider(params.provider),
|
|
341
|
+
reasoning: false,
|
|
342
|
+
input: ["text"],
|
|
343
|
+
cost: {
|
|
344
|
+
input: 0,
|
|
345
|
+
output: 0,
|
|
346
|
+
cacheRead: 0,
|
|
347
|
+
cacheWrite: 0,
|
|
348
|
+
},
|
|
349
|
+
contextWindow: 200_000,
|
|
350
|
+
maxTokens: 8_000,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Normalize an auth result down to the API key that pi-ai expects. */
|
|
355
|
+
function resolveApiKeyFromAuthResult(auth: RuntimeModelAuthResult | undefined): string | undefined {
|
|
356
|
+
const apiKey = auth?.apiKey?.trim();
|
|
357
|
+
return apiKey ? apiKey : undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function buildLegacyAuthFallbackWarning(): string {
|
|
361
|
+
return [
|
|
362
|
+
"[lcm] OpenClaw runtime.modelAuth is unavailable; using legacy auth-profiles fallback.",
|
|
363
|
+
`Stock lossless-claw 0.2.7 expects OpenClaw plugin runtime support from PR #41090 (${MODEL_AUTH_PR_URL}).`,
|
|
364
|
+
`OpenClaw 2026.3.8 and 2026.3.8-beta.1 do not include merge commit ${MODEL_AUTH_MERGE_COMMIT};`,
|
|
365
|
+
`${MODEL_AUTH_REQUIRED_RELEASE} is required for stock lossless-claw 0.2.7 without this fallback patch.`,
|
|
366
|
+
].join(" ");
|
|
367
|
+
}
|
|
368
|
+
|
|
282
369
|
/** Parse auth-profiles JSON into a minimal store shape. */
|
|
283
370
|
function parseAuthProfileStore(raw: string): AuthProfileStore | undefined {
|
|
284
371
|
try {
|
|
@@ -618,6 +705,7 @@ function readLatestAssistantReply(messages: unknown[]): string | undefined {
|
|
|
618
705
|
function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
619
706
|
const envSnapshot = snapshotPluginEnv();
|
|
620
707
|
envSnapshot.openclawDefaultModel = readDefaultModelFromConfig(api.config);
|
|
708
|
+
const modelAuth = getRuntimeModelAuth(api);
|
|
621
709
|
const readEnv: ReadEnvFn = (key) => process.env[key];
|
|
622
710
|
const pluginConfig =
|
|
623
711
|
api.pluginConfig && typeof api.pluginConfig === "object" && !Array.isArray(api.pluginConfig)
|
|
@@ -637,6 +725,10 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
637
725
|
}
|
|
638
726
|
}
|
|
639
727
|
|
|
728
|
+
if (!modelAuth) {
|
|
729
|
+
api.logger.warn(buildLegacyAuthFallbackWarning());
|
|
730
|
+
}
|
|
731
|
+
|
|
640
732
|
return {
|
|
641
733
|
config,
|
|
642
734
|
complete: async ({
|
|
@@ -713,11 +805,30 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
713
805
|
maxTokens: 8_000,
|
|
714
806
|
};
|
|
715
807
|
|
|
716
|
-
let resolvedApiKey = apiKey?.trim()
|
|
717
|
-
if (!resolvedApiKey &&
|
|
808
|
+
let resolvedApiKey = apiKey?.trim();
|
|
809
|
+
if (!resolvedApiKey && modelAuth) {
|
|
810
|
+
try {
|
|
811
|
+
resolvedApiKey = resolveApiKeyFromAuthResult(
|
|
812
|
+
await modelAuth.resolveApiKeyForProvider({
|
|
813
|
+
provider: providerId,
|
|
814
|
+
cfg: api.config,
|
|
815
|
+
...(authProfileId ? { profileId: authProfileId } : {}),
|
|
816
|
+
}),
|
|
817
|
+
);
|
|
818
|
+
} catch (err) {
|
|
819
|
+
console.error(
|
|
820
|
+
`[lcm] modelAuth.resolveApiKeyForProvider FAILED:`,
|
|
821
|
+
err instanceof Error ? err.message : err,
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (!resolvedApiKey && !modelAuth) {
|
|
826
|
+
resolvedApiKey = resolveApiKey(providerId, readEnv);
|
|
827
|
+
}
|
|
828
|
+
if (!resolvedApiKey && !modelAuth && typeof mod.getEnvApiKey === "function") {
|
|
718
829
|
resolvedApiKey = mod.getEnvApiKey(providerId)?.trim();
|
|
719
830
|
}
|
|
720
|
-
if (!resolvedApiKey) {
|
|
831
|
+
if (!resolvedApiKey && !modelAuth) {
|
|
721
832
|
resolvedApiKey = await resolveApiKeyFromAuthProfiles({
|
|
722
833
|
provider: providerId,
|
|
723
834
|
authProfileId,
|
|
@@ -849,11 +960,79 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
849
960
|
).trim();
|
|
850
961
|
return { provider, model: raw };
|
|
851
962
|
},
|
|
852
|
-
getApiKey: (provider) =>
|
|
853
|
-
|
|
854
|
-
|
|
963
|
+
getApiKey: async (provider, model, options) => {
|
|
964
|
+
if (modelAuth) {
|
|
965
|
+
try {
|
|
966
|
+
const modelAuthKey = resolveApiKeyFromAuthResult(
|
|
967
|
+
await modelAuth.getApiKeyForModel({
|
|
968
|
+
model: buildModelAuthLookupModel({ provider, model }),
|
|
969
|
+
cfg: api.config,
|
|
970
|
+
...(options?.profileId ? { profileId: options.profileId } : {}),
|
|
971
|
+
...(options?.preferredProfile ? { preferredProfile: options.preferredProfile } : {}),
|
|
972
|
+
}),
|
|
973
|
+
);
|
|
974
|
+
if (modelAuthKey) {
|
|
975
|
+
return modelAuthKey;
|
|
976
|
+
}
|
|
977
|
+
} catch {
|
|
978
|
+
// Fall through to auth-profile lookup for older OpenClaw runtimes.
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const envKey = resolveApiKey(provider, readEnv);
|
|
983
|
+
if (envKey) {
|
|
984
|
+
return envKey;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const piAiModuleId = "@mariozechner/pi-ai";
|
|
988
|
+
const mod = (await import(piAiModuleId)) as PiAiModule;
|
|
989
|
+
return resolveApiKeyFromAuthProfiles({
|
|
990
|
+
provider,
|
|
991
|
+
authProfileId: options?.profileId,
|
|
992
|
+
agentDir: api.resolvePath("."),
|
|
993
|
+
runtimeConfig: api.config,
|
|
994
|
+
piAiModule: mod,
|
|
995
|
+
envSnapshot,
|
|
996
|
+
});
|
|
997
|
+
},
|
|
998
|
+
requireApiKey: async (provider, model, options) => {
|
|
999
|
+
const key = await (async () => {
|
|
1000
|
+
if (modelAuth) {
|
|
1001
|
+
try {
|
|
1002
|
+
const modelAuthKey = resolveApiKeyFromAuthResult(
|
|
1003
|
+
await modelAuth.getApiKeyForModel({
|
|
1004
|
+
model: buildModelAuthLookupModel({ provider, model }),
|
|
1005
|
+
cfg: api.config,
|
|
1006
|
+
...(options?.profileId ? { profileId: options.profileId } : {}),
|
|
1007
|
+
...(options?.preferredProfile ? { preferredProfile: options.preferredProfile } : {}),
|
|
1008
|
+
}),
|
|
1009
|
+
);
|
|
1010
|
+
if (modelAuthKey) {
|
|
1011
|
+
return modelAuthKey;
|
|
1012
|
+
}
|
|
1013
|
+
} catch {
|
|
1014
|
+
// Fall through to auth-profile lookup for older OpenClaw runtimes.
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const envKey = resolveApiKey(provider, readEnv);
|
|
1019
|
+
if (envKey) {
|
|
1020
|
+
return envKey;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const piAiModuleId = "@mariozechner/pi-ai";
|
|
1024
|
+
const mod = (await import(piAiModuleId)) as PiAiModule;
|
|
1025
|
+
return resolveApiKeyFromAuthProfiles({
|
|
1026
|
+
provider,
|
|
1027
|
+
authProfileId: options?.profileId,
|
|
1028
|
+
agentDir: api.resolvePath("."),
|
|
1029
|
+
runtimeConfig: api.config,
|
|
1030
|
+
piAiModule: mod,
|
|
1031
|
+
envSnapshot,
|
|
1032
|
+
});
|
|
1033
|
+
})();
|
|
855
1034
|
if (!key) {
|
|
856
|
-
throw new Error(`Missing API key for provider '${provider}'.`);
|
|
1035
|
+
throw new Error(`Missing API key for provider '${provider}' (model '${model}').`);
|
|
857
1036
|
}
|
|
858
1037
|
return key;
|
|
859
1038
|
},
|
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.8",
|
|
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.
|