mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-10 12:34:27 +00:00
fix(offload): resolve local-llm apiKey from auth-profile store as fallback
When offload.model references a provider whose apiKey is managed via OpenClaw's auth-profiles mechanism (the recommended default, e.g. set up with 'openclaw auth'), the local-llm initializer previously only read models.providers[provider].apiKey in the config tree. If the key was absent there, LocalLlmClient construction was skipped and L1/L1.5/L2 were disabled with a misleading "missing apiKey" error -- even though the key existed and the main agent model worked fine. Fix: add src/offload/auth-profile-key.ts, a synchronous helper that loads the auth-profile store via openclaw/plugin-sdk/provider-auth and returns the first api_key-type credential for the provider. The helper is guarded with try/catch so older OpenClaw versions that do not expose this SDK subpath degrade silently to the previous behavior. In index.ts, the apiKey lookup becomes: providerCfg?.apiKey ?? resolveApiKeyFromAuthProfile(api, providerKey, logger) registerOffload() keeps its synchronous contract; no race is introduced around backendClient assignment. Closes #90 Signed-off-by: MicroGrey <changyuhang@outlook.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Auth-profile API key resolver for offload local mode.
|
||||
*
|
||||
* OpenClaw stores model credentials in two places:
|
||||
* 1. `models.providers[provider].apiKey` — plaintext in openclaw.json
|
||||
* 2. auth-profiles store — the credential "vault" populated by `openclaw auth`
|
||||
*
|
||||
* The offload local-llm path historically only read location (1). When users
|
||||
* manage their keys via auth-profiles (location 2) — the OpenClaw-recommended
|
||||
* default — the lookup misses and L1/L1.5/L2 get disabled (see issue #90).
|
||||
*
|
||||
* This module provides a SYNCHRONOUS fallback that reads the key from the
|
||||
* auth-profile store, so `registerOffload` keeps its synchronous contract and
|
||||
* no race is introduced around `backendClient`.
|
||||
*
|
||||
* Compatibility: the `openclaw/plugin-sdk/provider-auth` subpath only exists on
|
||||
* newer OpenClaw versions. All host calls are guarded so that on older hosts
|
||||
* (or any unexpected failure) we silently fall back to the previous behavior
|
||||
* of "config-tree only".
|
||||
*/
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
import type { PluginLogger } from "./types.js";
|
||||
|
||||
const TAG = "[context-offload] [auth-profile]";
|
||||
|
||||
// The plugin is ESM ("type": "module"), so `require` is not a global. Create a
|
||||
// CJS require bound to this module's URL — matches the pattern in the plugin
|
||||
// entry (index.ts) and lets us tolerate a missing SDK subpath on older hosts.
|
||||
const _require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* Resolve an API key for `providerKey` from OpenClaw's auth-profile store.
|
||||
*
|
||||
* Returns the plaintext key for the first `api_key`-type profile bound to the
|
||||
* provider, or `undefined` when nothing usable is found (no profile, only
|
||||
* oauth/token credentials, indirect keyRef-only storage, or an older host that
|
||||
* does not expose the auth-profile SDK).
|
||||
*
|
||||
* This is intentionally synchronous: the underlying store loaders
|
||||
* (`ensureAuthProfileStore`, `listProfilesForProvider`) are synchronous, which
|
||||
* lets the caller resolve the key inline without awaiting.
|
||||
*
|
||||
* @param api - OpenClaw plugin api (its `config` is forwarded to the resolver).
|
||||
* @param providerKey - Provider name parsed from the model ref (e.g. "xiaomi").
|
||||
* @param logger - Optional logger; failures are reported at debug level.
|
||||
*/
|
||||
export function resolveApiKeyFromAuthProfile(
|
||||
api: { config?: unknown },
|
||||
providerKey: string,
|
||||
logger?: PluginLogger,
|
||||
): string | undefined {
|
||||
try {
|
||||
// Lazily load the SDK subpath so a missing export on older OpenClaw
|
||||
// versions degrades gracefully instead of crashing module load.
|
||||
const sdk = loadProviderAuthSdk();
|
||||
if (!sdk) return undefined;
|
||||
|
||||
const { ensureAuthProfileStore, listProfilesForProvider, resolveOpenClawAgentDir } = sdk;
|
||||
if (
|
||||
typeof ensureAuthProfileStore !== "function" ||
|
||||
typeof listProfilesForProvider !== "function"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const agentDir =
|
||||
typeof resolveOpenClawAgentDir === "function" ? resolveOpenClawAgentDir() : undefined;
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir, { config: api.config });
|
||||
if (!store || typeof store !== "object") return undefined;
|
||||
|
||||
const profileIds = listProfilesForProvider(store, providerKey);
|
||||
if (!Array.isArray(profileIds) || profileIds.length === 0) return undefined;
|
||||
|
||||
const profiles = (store as { profiles?: Record<string, unknown> }).profiles ?? {};
|
||||
for (const id of profileIds) {
|
||||
const cred = profiles[id] as { type?: string; key?: string } | undefined;
|
||||
// Only api_key credentials carry a directly-usable plaintext key.
|
||||
// oauth/token profiles, or api_key profiles that store only a keyRef
|
||||
// (indirect/keychain), cannot be consumed by the OpenAI-compatible
|
||||
// local-llm caller, so we skip them.
|
||||
if (cred?.type === "api_key" && typeof cred.key === "string" && cred.key.length > 0) {
|
||||
logger?.debug?.(`${TAG} Resolved api key for provider "${providerKey}" from profile "${id}"`);
|
||||
return cred.key;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
logger?.debug?.(
|
||||
`${TAG} Auth-profile lookup unavailable for provider "${providerKey}": ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
interface ProviderAuthSdk {
|
||||
ensureAuthProfileStore?: (
|
||||
agentDir?: string,
|
||||
options?: { config?: unknown },
|
||||
) => { profiles?: Record<string, unknown> } | null | undefined;
|
||||
listProfilesForProvider?: (store: unknown, provider: string) => string[];
|
||||
resolveOpenClawAgentDir?: () => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the `openclaw/plugin-sdk/provider-auth` subpath.
|
||||
*
|
||||
* Uses the module-scoped CJS require so a missing subpath (older hosts)
|
||||
* surfaces as a caught error rather than an unhandled module-resolution
|
||||
* failure.
|
||||
*/
|
||||
function loadProviderAuthSdk(): ProviderAuthSdk | undefined {
|
||||
try {
|
||||
return _require("openclaw/plugin-sdk/provider-auth") as ProviderAuthSdk;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ import type { OffloadConfig } from "../config.js";
|
||||
import type { PluginConfig, PluginLogger } from "./types.js";
|
||||
import { BackendClient } from "./backend-client.js";
|
||||
import { LocalLlmClient } from "./local-llm/index.js";
|
||||
import { resolveApiKeyFromAuthProfile } from "./auth-profile-key.js";
|
||||
import type { L1Request, L15Request, L2Request } from "./backend-client.js";
|
||||
import { parseMmdMeta } from "./mmd-meta.js";
|
||||
import { sanitizeText, writeRefMd } from "./storage.js";
|
||||
@@ -368,7 +369,10 @@ export function registerOffload(api: any, offloadConfig: OffloadConfig): void {
|
||||
const models = (api.config as any)?.models;
|
||||
const providerCfg = models?.providers?.[providerKey];
|
||||
const baseUrl = providerCfg?.baseUrl ?? providerCfg?.baseURL;
|
||||
const apiKey = providerCfg?.apiKey;
|
||||
// Key resolution: prefer the plaintext key in models.providers, then fall
|
||||
// back to OpenClaw's auth-profile store (issue #90). The fallback is a
|
||||
// synchronous no-op on hosts that don't expose the auth-profile SDK.
|
||||
const apiKey = providerCfg?.apiKey ?? resolveApiKeyFromAuthProfile(api, providerKey, logger);
|
||||
|
||||
if (baseUrl && apiKey) {
|
||||
backendClient = new LocalLlmClient(
|
||||
@@ -377,7 +381,7 @@ export function registerOffload(api: any, offloadConfig: OffloadConfig): void {
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
`[context-offload] Local LLM mode failed: provider "${providerKey}" not found or missing baseUrl/apiKey in models.providers. ` +
|
||||
`[context-offload] Local LLM mode failed: provider "${providerKey}" not found or missing baseUrl/apiKey in models.providers (or auth profiles). ` +
|
||||
`L1/L1.5/L2 disabled.`,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user