@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 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() || resolveApiKey(providerId, readEnv);
717
- if (!resolvedApiKey && typeof mod.getEnvApiKey === "function") {
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) => resolveApiKey(provider, readEnv),
853
- requireApiKey: (provider) => {
854
- const key = resolveApiKey(provider, readEnv);
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.6",
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 GetApiKeyFn = (provider: string, model: string) => string | undefined;
62
- export type RequireApiKeyFn = (provider: string, model: string) => string;
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.