// Lazy-load pi-coding-agent model metadata so we can infer context windows when // the agent reports a model id. This includes custom models.json entries.
import path from "node:path"; import { loadConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { computeBackoff, type BackoffPolicy } from "../infra/backoff.js"; import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { resolveOpenClawAgentDir } from "./agent-paths.js"; import { lookupCachedContextTokens, MODEL_CONTEXT_TOKEN_CACHE } from "./context-cache.js"; import { CONTEXT_WINDOW_RUNTIME_STATE } from "./context-runtime-state.js"; import { normalizeProviderId } from "./model-selection.js";
export { resetContextWindowCacheForTest } from "./context-runtime-state.js";
type ModelEntry = { id: string; contextWindow?: number; contextTokens?: number };
type ModelRegistryLike = {
getAvailable?: () => ModelEntry[];
getAll: () => ModelEntry[];
};
type ConfigModelEntry = { id?: string; contextWindow?: number; contextTokens?: number };
type ProviderConfigEntry = { models?: ConfigModelEntry[] };
type ModelsConfig = { providers?: Record<string, ProviderConfigEntry | undefined> };
type AgentModelEntry = { params?: Record<string, unknown> };
export function shouldEagerWarmContextWindowCache(argv: string[] = process.argv): boolean { // Keep this gate tied to the real OpenClaw CLI entrypoints. // // This module can also land inside shared dist chunks that are imported from // plugin-sdk/library surfaces during smoke tests and plugin loading. If we do // eager warmup for those generic Node script imports, merely importing the // built plugin-sdk can call ensureOpenClawModelsJson(), which cascades into // plugin discovery and breaks dist/source singleton assumptions. if (!isLikelyOpenClawCliProcess(argv)) { returnfalse;
} const [primary] = getCommandPathFromArgv(argv); returnBoolean(primary) && !SKIP_EAGER_WARMUP_PRIMARY_COMMANDS.has(primary);
}
function ensureContextWindowCacheLoaded(): Promise<void> { if (CONTEXT_WINDOW_RUNTIME_STATE.loadPromise) { return CONTEXT_WINDOW_RUNTIME_STATE.loadPromise;
}
const cfg = primeConfiguredContextWindows(); if (!cfg) { return Promise.resolve();
}
export function lookupContextTokens(
modelId?: string,
options?: { allowAsyncLoad?: boolean },
): number | undefined { if (!modelId) { return undefined;
} if (options?.allowAsyncLoad === false) { // Read-only callers still need synchronous config-backed overrides, but they // should not start background model discovery or models.json writes.
primeConfiguredContextWindows();
} else { // Best-effort: kick off loading on demand, but don't block lookups. void ensureContextWindowCacheLoaded();
} return lookupCachedContextTokens(modelId);
}
if (shouldEagerWarmContextWindowCache()) { // Keep startup warmth for the real CLI, but avoid import-time side effects // when this module is pulled in through library/plugin-sdk surfaces. void ensureContextWindowCacheLoaded();
}
// Look up an explicit runtime context cap for a specific provider+model // directly from config, without going through the shared discovery cache. // This avoids the cache keyspace collision where "provider/model" synthetic // keys overlap with raw slash-containing model IDs (e.g. OpenRouter's // "google/gemini-2.5-pro" stored as a raw catalog entry). function resolveConfiguredProviderContextTokens(
cfg: OpenClawConfig | undefined,
provider: string,
model: string,
): number | undefined { const providers = (cfg?.models as ModelsConfig | undefined)?.providers; if (!providers) { return undefined;
}
// Mirror the lookup order in pi-embedded-runner/model.ts: exact key first, // then normalized fallback. This prevents alias collisions from picking the // wrong configured cap based on Object.entries iteration order. function findContextTokens(matchProviderId: (id: string) => boolean): number | undefined { for (const [providerId, providerConfig] of Object.entries(providers!)) { if (!matchProviderId(providerId)) { continue;
} if (!Array.isArray(providerConfig?.models)) { continue;
} for (const m of providerConfig.models) { const contextTokens = typeof m?.contextTokens === "number"
? m.contextTokens
: typeof m?.contextWindow === "number"
? m.contextWindow
: undefined; if ( typeof m?.id === "string" &&
m.id === model && typeof contextTokens === "number" &&
contextTokens > 0
) { return contextTokens;
}
}
} return undefined;
}
// 1. Exact match (case-insensitive, no alias expansion). const exactResult = findContextTokens(
(id) => normalizeLowercaseStringOrEmpty(id) === normalizeLowercaseStringOrEmpty(provider),
); if (exactResult !== undefined) { return exactResult;
}
// 2. Normalized fallback: covers alias keys such as "z.ai" → "zai". const normalizedProvider = normalizeProviderId(provider); return findContextTokens((id) => normalizeProviderId(id) === normalizedProvider);
}
const ref = resolveProviderModelRef({
provider: params.provider,
model: params.model,
}); const explicitProvider = params.provider?.trim(); if (ref) { const modelParams = resolveConfiguredModelParams(params.cfg, ref.provider, ref.model); if (modelParams?.context1m === true && isAnthropic1MModel(ref.provider, ref.model)) { return ANTHROPIC_CONTEXT_1M_TOKENS;
} // Only do the config direct scan when the caller explicitly passed a // provider. When provider is inferred from a slash in the model string // (e.g. "google/gemini-2.5-pro" → ref.provider = "google"), the model ID // may belong to a DIFFERENT provider (e.g. an OpenRouter session). Scanning // cfg.models.providers.google in that case would return Google's configured // window and misreport context limits for the OpenRouter session. // See status.ts log-usage fallback which calls with only { model } set. if (explicitProvider) { const configuredWindow = resolveConfiguredProviderContextTokens(
params.cfg,
explicitProvider,
ref.model,
); if (configuredWindow !== undefined) { return configuredWindow;
}
}
}
if (explicitProvider && ref && shouldUseAnthropicOpus47ContextWindow(ref)) { return ANTHROPIC_CONTEXT_1M_TOKENS;
}
// When provider is explicitly given and the model ID is bare (no slash), // try the provider-qualified cache key BEFORE the bare key. Discovery // entries are stored under qualified IDs (e.g. "google-gemini-cli/ // gemini-3.1-pro-preview → 1M"), while the bare key may hold a cross- // provider minimum (128k). Returning the qualified entry gives the correct // provider-specific window for /status and session context-token persistence. // // Guard: only when params.provider is explicit (not inferred from a slash in // the model string). For model-only callers (e.g. status.ts log-usage // fallback with model="google/gemini-2.5-pro"), the inferred provider would // construct "google/gemini-2.5-pro" as the qualified key which accidentally // matches OpenRouter's raw discovery entry — the bare lookup is correct there. if (params.provider && ref && !ref.model.includes("/")) { const qualifiedResult = lookupContextTokens(
`${normalizeProviderId(ref.provider)}/${ref.model}`,
{ allowAsyncLoad: params.allowAsyncLoad },
); if (qualifiedResult !== undefined) { return qualifiedResult;
}
}
// Bare key fallback. For model-only calls with slash-containing IDs // (e.g. "google/gemini-2.5-pro") this IS the raw discovery cache key. const bareResult = lookupContextTokens(params.model, {
allowAsyncLoad: params.allowAsyncLoad,
}); if (bareResult !== undefined) { return bareResult;
}
// When provider is implicit, try qualified as a last resort so inferred // provider/model pairs (e.g. model="google-gemini-cli/gemini-3.1-pro") // still find discovery entries stored under that qualified ID. if (!params.provider && ref && !ref.model.includes("/")) { const qualifiedResult = lookupContextTokens(
`${normalizeProviderId(ref.provider)}/${ref.model}`,
{ allowAsyncLoad: params.allowAsyncLoad },
); if (qualifiedResult !== undefined) { return qualifiedResult;
}
}
return params.fallbackContextTokens;
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.1 Sekunden
(vorverarbeitet am 2026-05-26)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.