Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 84 additions & 18 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,37 @@ function resolveProviderApiFromRuntimeConfig(
return typeof api === "string" && api.trim() ? api.trim() : undefined;
}

/** Resolve full provider config from runtime config if available. */
function resolveProviderConfigFromRuntimeConfig(
runtimeConfig: unknown,
provider: string,
): Record<string, unknown> | undefined {
if (!isRecord(runtimeConfig)) {
return undefined;
}
const providers = (runtimeConfig as { models?: { providers?: Record<string, unknown> } }).models
?.providers;
if (!providers || !isRecord(providers)) {
return undefined;
}
const value = findProviderConfigValue(providers, provider);
return isRecord(value) ? value : undefined;
}

/** Pi-ai expects Ollama through the OpenAI-compatible /v1 surface. */
function normalizeOllamaBaseUrl(baseUrl: string | undefined): string | undefined {
const trimmed = baseUrl?.trim();
if (!trimmed) {
return undefined;
}

const withoutTrailingSlash = trimmed.replace(/\/+$/, "");
if (/\/v1$/i.test(withoutTrailingSlash)) {
return withoutTrailingSlash;
}
return `${withoutTrailingSlash}/v1`;
}

/** Resolve runtime.modelAuth from plugin runtime when available. */
function getRuntimeModelAuth(api: OpenClawPluginApi): RuntimeModelAuth | undefined {
const runtime = api.runtime as OpenClawPluginApi["runtime"] & {
Expand Down Expand Up @@ -897,11 +928,54 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
}
}

const loadedRuntimeConfig = (() => {
try {
return api.runtime.config.loadConfig();
} catch {
return undefined;
}
})();

const providerConfig =
resolveProviderConfigFromRuntimeConfig(effectiveRuntimeConfig, providerId) ||
resolveProviderConfigFromRuntimeConfig(api.config, providerId) ||
resolveProviderConfigFromRuntimeConfig(loadedRuntimeConfig, providerId);
let effectiveProviderApi =
providerApi?.trim() ||
resolveProviderApiFromRuntimeConfig(effectiveRuntimeConfig, providerId) ||
resolveProviderApiFromRuntimeConfig(api.config, providerId) ||
resolveProviderApiFromRuntimeConfig(loadedRuntimeConfig, providerId);
let providerLevelConfig: Record<string, unknown> = providerConfig ? { ...providerConfig } : {};
let preferredApiKey = apiKey?.trim();

if (
normalizeProviderId(providerId) === "ollama" &&
normalizeProviderId(effectiveProviderApi ?? "ollama") === "ollama"
) {
effectiveProviderApi = "openai-completions";
const normalizedBaseUrl = normalizeOllamaBaseUrl(
typeof providerLevelConfig.baseUrl === "string" ? providerLevelConfig.baseUrl : undefined,
);
providerLevelConfig = {
...providerLevelConfig,
api: "openai-completions",
...(normalizedBaseUrl ? { baseUrl: normalizedBaseUrl } : {}),
};
if (!preferredApiKey && typeof providerConfig?.apiKey === "string" && providerConfig.apiKey.trim()) {
preferredApiKey = providerConfig.apiKey.trim();
}
if (!preferredApiKey) {
preferredApiKey = "ollama-local";
}
}

const knownModel =
typeof mod.getModel === "function" ? mod.getModel(providerId, modelId) : undefined;
const fallbackApi =
providerApi?.trim() ||
resolveProviderApiFromRuntimeConfig(effectiveRuntimeConfig, providerId) ||
effectiveProviderApi?.trim() ||
(typeof providerLevelConfig.api === "string" && providerLevelConfig.api.trim()
? providerLevelConfig.api.trim()
: undefined) ||
(() => {
if (typeof mod.getModels !== "function") {
return undefined;
Expand All @@ -915,20 +989,12 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
})() ||
inferApiFromProvider(providerId);

// Resolve provider-level config (baseUrl, headers, etc.) from runtime config.
// Custom/proxy providers (e.g. bailian, local proxies) store their baseUrl and
// apiKey under models.providers.<provider> in openclaw.json. Without this
// lookup the resolved model object lacks baseUrl, which crashes pi-ai's
// detectCompat() ("Cannot read properties of undefined (reading 'includes')"),
// and the apiKey is unresolvable, causing 401 errors. See #19.
const providerLevelConfig: Record<string, unknown> = (() => {
if (!isRecord(effectiveRuntimeConfig)) return {};
const providers = (effectiveRuntimeConfig as { models?: { providers?: Record<string, unknown> } })
.models?.providers;
if (!providers) return {};
const cfg = findProviderConfigValue(providers, providerId);
return isRecord(cfg) ? cfg : {};
})();
const resolvedKnownModelApi =
effectiveProviderApi?.trim() ||
(typeof providerLevelConfig.api === "string" && providerLevelConfig.api.trim()
? providerLevelConfig.api.trim()
: undefined) ||
knownModel.api;

const resolvedModel =
isRecord(knownModel) &&
Expand All @@ -939,7 +1005,7 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
...knownModel,
id: knownModel.id,
provider: knownModel.provider,
api: knownModel.api,
api: resolvedKnownModelApi,
// Merge baseUrl/headers from provider config if not already on the model.
// Always set baseUrl to a string — pi-ai's detectCompat() crashes when
// baseUrl is undefined.
Expand Down Expand Up @@ -978,7 +1044,7 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
: {}),
};

let resolvedApiKey = apiKey?.trim();
let resolvedApiKey = preferredApiKey;
if (!resolvedApiKey && modelAuth) {
try {
resolvedApiKey = resolveApiKeyFromAuthResult(
Expand Down
145 changes: 121 additions & 24 deletions src/summarize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,23 @@ function normalizeProviderId(provider: string): string {
return provider.trim().toLowerCase();
}

/**
* Resolve provider API override from legacy OpenClaw config.
*
* When model ids are custom/forward-compat, this hint allows deps.complete to
* construct a valid pi-ai Model object even if getModel(provider, model) misses.
*/
function resolveProviderApiFromLegacyConfig(
type LegacyProviderConfig = Record<string, unknown> & {
api?: unknown;
baseUrl?: unknown;
apiKey?: unknown;
};

type SummarizerRuntimeProviderAdaptation = {
providerApi?: string;
runtimeConfig: unknown;
apiKey?: string;
};

/** Resolve full provider config from legacy OpenClaw runtime config. */
function resolveProviderConfigFromLegacyConfig(
config: unknown,
provider: string,
): string | undefined {
): LegacyProviderConfig | undefined {
if (!config || typeof config !== "object") {
return undefined;
}
Expand All @@ -58,10 +65,7 @@ function resolveProviderApiFromLegacyConfig(

const direct = providers[provider];
if (direct && typeof direct === "object") {
const api = (direct as { api?: unknown }).api;
if (typeof api === "string" && api.trim()) {
return api.trim();
}
return direct as LegacyProviderConfig;
}

const normalizedProvider = normalizeProviderId(provider);
Expand All @@ -72,14 +76,103 @@ function resolveProviderApiFromLegacyConfig(
if (!value || typeof value !== "object") {
continue;
}
const api = (value as { api?: unknown }).api;
if (typeof api === "string" && api.trim()) {
return api.trim();
}
return value as LegacyProviderConfig;
}
return undefined;
}

/** Copy legacy runtime config while overriding a single provider entry. */
function overrideLegacyProviderConfig(
runtimeConfig: unknown,
provider: string,
override: LegacyProviderConfig,
): unknown {
if (!isRecord(runtimeConfig)) {
return runtimeConfig;
}

const models = isRecord(runtimeConfig.models) ? runtimeConfig.models : {};
const providers = isRecord(models.providers) ? models.providers : {};
const nextProviders: Record<string, unknown> = { ...providers };

let providerKey = provider;
const normalizedProvider = normalizeProviderId(provider);
for (const key of Object.keys(providers)) {
if (normalizeProviderId(key) === normalizedProvider) {
providerKey = key;
break;
}
}

nextProviders[providerKey] = override;

return {
...runtimeConfig,
models: {
...models,
providers: nextProviders,
},
};
}

/** Pi-ai expects Ollama through the OpenAI-compatible `/v1` surface. */
function normalizeOllamaBaseUrl(baseUrl: string | undefined): string | undefined {
const trimmed = baseUrl?.trim();
if (!trimmed) {
return undefined;
}

const withoutTrailingSlash = trimmed.replace(/\/+$/, "");
if (/\/v1$/i.test(withoutTrailingSlash)) {
return withoutTrailingSlash;
}
return `${withoutTrailingSlash}/v1`;
}

/** Adapt native OpenClaw Ollama provider config into pi-ai compatible inputs. */
function adaptSummarizerRuntimeProvider(params: {
runtimeConfig: unknown;
provider: string;
apiKey?: string;
}): SummarizerRuntimeProviderAdaptation {
const providerConfig = resolveProviderConfigFromLegacyConfig(params.runtimeConfig, params.provider);
const providerApi =
typeof providerConfig?.api === "string" && providerConfig.api.trim()
? providerConfig.api.trim()
: undefined;

if (!providerApi || normalizeProviderId(providerApi) !== "ollama") {
return {
providerApi,
runtimeConfig: params.runtimeConfig,
apiKey: params.apiKey,
};
}

const normalizedBaseUrl = normalizeOllamaBaseUrl(
typeof providerConfig.baseUrl === "string" ? providerConfig.baseUrl : undefined,
);
const adaptedProviderConfig: LegacyProviderConfig = {
...providerConfig,
api: "openai-completions",
...(normalizedBaseUrl ? { baseUrl: normalizedBaseUrl } : {}),
};
const configApiKey =
typeof providerConfig.apiKey === "string" && providerConfig.apiKey.trim()
? providerConfig.apiKey.trim()
: undefined;

return {
providerApi: "openai-completions",
runtimeConfig: overrideLegacyProviderConfig(
params.runtimeConfig,
params.provider,
adaptedProviderConfig,
),
apiKey: params.apiKey ?? configApiKey ?? "ollama-local",
};
}

/** Approximate token estimate used for target-sizing prompts. */
function estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
Expand Down Expand Up @@ -670,7 +763,6 @@ export async function createLcmSummarizeFromLegacyParams(params: {
typeof params.legacyParams.agentDir === "string" && params.legacyParams.agentDir.trim()
? params.legacyParams.agentDir.trim()
: undefined;
const providerApi = resolveProviderApiFromLegacyConfig(params.legacyParams.config, provider);

const condensedTargetTokens =
Number.isFinite(params.deps.config.condensedTargetTokens) &&
Expand All @@ -689,9 +781,14 @@ export async function createLcmSummarizeFromLegacyParams(params: {

const mode: SummaryMode = aggressive ? "aggressive" : "normal";
const isCondensed = options?.isCondensed === true;
const apiKey = await params.deps.getApiKey(provider, model, {
const resolvedApiKey = await params.deps.getApiKey(provider, model, {
profileId: authProfileId,
});
const runtimeProvider = adaptSummarizerRuntimeProvider({
runtimeConfig: params.legacyParams.config,
provider,
apiKey: resolvedApiKey,
});
const targetTokens = resolveTargetTokens({
inputTokens: estimateTokens(text),
mode,
Expand Down Expand Up @@ -720,11 +817,11 @@ export async function createLcmSummarizeFromLegacyParams(params: {
const result = await params.deps.complete({
provider,
model,
apiKey,
providerApi,
apiKey: runtimeProvider.apiKey,
providerApi: runtimeProvider.providerApi,
authProfileId,
agentDir,
runtimeConfig: params.legacyParams.config,
runtimeConfig: runtimeProvider.runtimeConfig,
system: LCM_SUMMARIZER_SYSTEM_PROMPT,
messages: [
{
Expand Down Expand Up @@ -778,11 +875,11 @@ export async function createLcmSummarizeFromLegacyParams(params: {
const retryResult = await params.deps.complete({
provider,
model,
apiKey,
providerApi,
apiKey: runtimeProvider.apiKey,
providerApi: runtimeProvider.providerApi,
authProfileId,
agentDir,
runtimeConfig: params.legacyParams.config,
runtimeConfig: runtimeProvider.runtimeConfig,
system: LCM_SUMMARIZER_SYSTEM_PROMPT,
messages: [
{
Expand Down
Loading