| import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js"; |
| import { parseModelRef } from "../agents/model-selection.js"; |
| import { resolveTalkApiKey } from "./talk.js"; |
| import type { OpenClawConfig } from "./types.js"; |
| import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; |
| import type { ModelDefinitionConfig } from "./types.models.js"; |
|
|
| type WarnState = { warned: boolean }; |
|
|
| let defaultWarnState: WarnState = { warned: false }; |
|
|
| type AnthropicAuthDefaultsMode = "api_key" | "oauth"; |
|
|
| const DEFAULT_MODEL_ALIASES: Readonly<Record<string, string>> = { |
| |
| opus: "anthropic/claude-opus-4-5", |
| sonnet: "anthropic/claude-sonnet-4-5", |
|
|
| |
| gpt: "openai/gpt-5.2", |
| "gpt-mini": "openai/gpt-5-mini", |
|
|
| |
| gemini: "google/gemini-3-pro-preview", |
| "gemini-flash": "google/gemini-3-flash-preview", |
| }; |
|
|
| const DEFAULT_MODEL_COST: ModelDefinitionConfig["cost"] = { |
| input: 0, |
| output: 0, |
| cacheRead: 0, |
| cacheWrite: 0, |
| }; |
| const DEFAULT_MODEL_INPUT: ModelDefinitionConfig["input"] = ["text"]; |
| const DEFAULT_MODEL_MAX_TOKENS = 8192; |
|
|
| type ModelDefinitionLike = Partial<ModelDefinitionConfig> & |
| Pick<ModelDefinitionConfig, "id" | "name">; |
|
|
| function isPositiveNumber(value: unknown): value is number { |
| return typeof value === "number" && Number.isFinite(value) && value > 0; |
| } |
|
|
| function resolveModelCost( |
| raw?: Partial<ModelDefinitionConfig["cost"]>, |
| ): ModelDefinitionConfig["cost"] { |
| return { |
| input: typeof raw?.input === "number" ? raw.input : DEFAULT_MODEL_COST.input, |
| output: typeof raw?.output === "number" ? raw.output : DEFAULT_MODEL_COST.output, |
| cacheRead: typeof raw?.cacheRead === "number" ? raw.cacheRead : DEFAULT_MODEL_COST.cacheRead, |
| cacheWrite: |
| typeof raw?.cacheWrite === "number" ? raw.cacheWrite : DEFAULT_MODEL_COST.cacheWrite, |
| }; |
| } |
|
|
| function resolveAnthropicDefaultAuthMode(cfg: OpenClawConfig): AnthropicAuthDefaultsMode | null { |
| const profiles = cfg.auth?.profiles ?? {}; |
| const anthropicProfiles = Object.entries(profiles).filter( |
| ([, profile]) => profile?.provider === "anthropic", |
| ); |
|
|
| const order = cfg.auth?.order?.anthropic ?? []; |
| for (const profileId of order) { |
| const entry = profiles[profileId]; |
| if (!entry || entry.provider !== "anthropic") { |
| continue; |
| } |
| if (entry.mode === "api_key") { |
| return "api_key"; |
| } |
| if (entry.mode === "oauth" || entry.mode === "token") { |
| return "oauth"; |
| } |
| } |
|
|
| const hasApiKey = anthropicProfiles.some(([, profile]) => profile?.mode === "api_key"); |
| const hasOauth = anthropicProfiles.some( |
| ([, profile]) => profile?.mode === "oauth" || profile?.mode === "token", |
| ); |
| if (hasApiKey && !hasOauth) { |
| return "api_key"; |
| } |
| if (hasOauth && !hasApiKey) { |
| return "oauth"; |
| } |
|
|
| if (process.env.ANTHROPIC_OAUTH_TOKEN?.trim()) { |
| return "oauth"; |
| } |
| if (process.env.ANTHROPIC_API_KEY?.trim()) { |
| return "api_key"; |
| } |
| return null; |
| } |
|
|
| function resolvePrimaryModelRef(raw?: string): string | null { |
| if (!raw || typeof raw !== "string") { |
| return null; |
| } |
| const trimmed = raw.trim(); |
| if (!trimmed) { |
| return null; |
| } |
| const aliasKey = trimmed.toLowerCase(); |
| return DEFAULT_MODEL_ALIASES[aliasKey] ?? trimmed; |
| } |
|
|
| export type SessionDefaultsOptions = { |
| warn?: (message: string) => void; |
| warnState?: WarnState; |
| }; |
|
|
| export function applyMessageDefaults(cfg: OpenClawConfig): OpenClawConfig { |
| const messages = cfg.messages; |
| const hasAckScope = messages?.ackReactionScope !== undefined; |
| if (hasAckScope) { |
| return cfg; |
| } |
|
|
| const nextMessages = messages ? { ...messages } : {}; |
| nextMessages.ackReactionScope = "group-mentions"; |
| return { |
| ...cfg, |
| messages: nextMessages, |
| }; |
| } |
|
|
| export function applySessionDefaults( |
| cfg: OpenClawConfig, |
| options: SessionDefaultsOptions = {}, |
| ): OpenClawConfig { |
| const session = cfg.session; |
| if (!session || session.mainKey === undefined) { |
| return cfg; |
| } |
|
|
| const trimmed = session.mainKey.trim(); |
| const warn = options.warn ?? console.warn; |
| const warnState = options.warnState ?? defaultWarnState; |
|
|
| const next: OpenClawConfig = { |
| ...cfg, |
| session: { ...session, mainKey: "main" }, |
| }; |
|
|
| if (trimmed && trimmed !== "main" && !warnState.warned) { |
| warnState.warned = true; |
| warn('session.mainKey is ignored; main session is always "main".'); |
| } |
|
|
| return next; |
| } |
|
|
| export function applyTalkApiKey(config: OpenClawConfig): OpenClawConfig { |
| const resolved = resolveTalkApiKey(); |
| if (!resolved) { |
| return config; |
| } |
| const existing = config.talk?.apiKey?.trim(); |
| if (existing) { |
| return config; |
| } |
| return { |
| ...config, |
| talk: { |
| ...config.talk, |
| apiKey: resolved, |
| }, |
| }; |
| } |
|
|
| export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig { |
| let mutated = false; |
| let nextCfg = cfg; |
|
|
| const providerConfig = nextCfg.models?.providers; |
| if (providerConfig) { |
| const nextProviders = { ...providerConfig }; |
| for (const [providerId, provider] of Object.entries(providerConfig)) { |
| const models = provider.models; |
| if (!Array.isArray(models) || models.length === 0) { |
| continue; |
| } |
| let providerMutated = false; |
| const nextModels = models.map((model) => { |
| const raw = model as ModelDefinitionLike; |
| let modelMutated = false; |
|
|
| const reasoning = typeof raw.reasoning === "boolean" ? raw.reasoning : false; |
| if (raw.reasoning !== reasoning) { |
| modelMutated = true; |
| } |
|
|
| const input = raw.input ?? [...DEFAULT_MODEL_INPUT]; |
| if (raw.input === undefined) { |
| modelMutated = true; |
| } |
|
|
| const cost = resolveModelCost(raw.cost); |
| const costMutated = |
| !raw.cost || |
| raw.cost.input !== cost.input || |
| raw.cost.output !== cost.output || |
| raw.cost.cacheRead !== cost.cacheRead || |
| raw.cost.cacheWrite !== cost.cacheWrite; |
| if (costMutated) { |
| modelMutated = true; |
| } |
|
|
| const contextWindow = isPositiveNumber(raw.contextWindow) |
| ? raw.contextWindow |
| : DEFAULT_CONTEXT_TOKENS; |
| if (raw.contextWindow !== contextWindow) { |
| modelMutated = true; |
| } |
|
|
| const defaultMaxTokens = Math.min(DEFAULT_MODEL_MAX_TOKENS, contextWindow); |
| const maxTokens = isPositiveNumber(raw.maxTokens) ? raw.maxTokens : defaultMaxTokens; |
| if (raw.maxTokens !== maxTokens) { |
| modelMutated = true; |
| } |
|
|
| if (!modelMutated) { |
| return model; |
| } |
| providerMutated = true; |
| return { |
| ...raw, |
| reasoning, |
| input, |
| cost, |
| contextWindow, |
| maxTokens, |
| } as ModelDefinitionConfig; |
| }); |
|
|
| if (!providerMutated) { |
| continue; |
| } |
| nextProviders[providerId] = { ...provider, models: nextModels }; |
| mutated = true; |
| } |
|
|
| if (mutated) { |
| nextCfg = { |
| ...nextCfg, |
| models: { |
| ...nextCfg.models, |
| providers: nextProviders, |
| }, |
| }; |
| } |
| } |
|
|
| const existingAgent = nextCfg.agents?.defaults; |
| if (!existingAgent) { |
| return mutated ? nextCfg : cfg; |
| } |
| const existingModels = existingAgent.models ?? {}; |
| if (Object.keys(existingModels).length === 0) { |
| return mutated ? nextCfg : cfg; |
| } |
|
|
| const nextModels: Record<string, { alias?: string }> = { |
| ...existingModels, |
| }; |
|
|
| for (const [alias, target] of Object.entries(DEFAULT_MODEL_ALIASES)) { |
| const entry = nextModels[target]; |
| if (!entry) { |
| continue; |
| } |
| if (entry.alias !== undefined) { |
| continue; |
| } |
| nextModels[target] = { ...entry, alias }; |
| mutated = true; |
| } |
|
|
| if (!mutated) { |
| return cfg; |
| } |
|
|
| return { |
| ...nextCfg, |
| agents: { |
| ...nextCfg.agents, |
| defaults: { ...existingAgent, models: nextModels }, |
| }, |
| }; |
| } |
|
|
| export function applyAgentDefaults(cfg: OpenClawConfig): OpenClawConfig { |
| const agents = cfg.agents; |
| const defaults = agents?.defaults; |
| const hasMax = |
| typeof defaults?.maxConcurrent === "number" && Number.isFinite(defaults.maxConcurrent); |
| const hasSubMax = |
| typeof defaults?.subagents?.maxConcurrent === "number" && |
| Number.isFinite(defaults.subagents.maxConcurrent); |
| if (hasMax && hasSubMax) { |
| return cfg; |
| } |
|
|
| let mutated = false; |
| const nextDefaults = defaults ? { ...defaults } : {}; |
| if (!hasMax) { |
| nextDefaults.maxConcurrent = DEFAULT_AGENT_MAX_CONCURRENT; |
| mutated = true; |
| } |
|
|
| const nextSubagents = defaults?.subagents ? { ...defaults.subagents } : {}; |
| if (!hasSubMax) { |
| nextSubagents.maxConcurrent = DEFAULT_SUBAGENT_MAX_CONCURRENT; |
| mutated = true; |
| } |
|
|
| if (!mutated) { |
| return cfg; |
| } |
|
|
| return { |
| ...cfg, |
| agents: { |
| ...agents, |
| defaults: { |
| ...nextDefaults, |
| subagents: nextSubagents, |
| }, |
| }, |
| }; |
| } |
|
|
| export function applyLoggingDefaults(cfg: OpenClawConfig): OpenClawConfig { |
| const logging = cfg.logging; |
| if (!logging) { |
| return cfg; |
| } |
| if (logging.redactSensitive) { |
| return cfg; |
| } |
| return { |
| ...cfg, |
| logging: { |
| ...logging, |
| redactSensitive: "tools", |
| }, |
| }; |
| } |
|
|
| export function applyContextPruningDefaults(cfg: OpenClawConfig): OpenClawConfig { |
| const defaults = cfg.agents?.defaults; |
| if (!defaults) { |
| return cfg; |
| } |
|
|
| const authMode = resolveAnthropicDefaultAuthMode(cfg); |
| if (!authMode) { |
| return cfg; |
| } |
|
|
| let mutated = false; |
| const nextDefaults = { ...defaults }; |
| const contextPruning = defaults.contextPruning ?? {}; |
| const heartbeat = defaults.heartbeat ?? {}; |
|
|
| if (defaults.contextPruning?.mode === undefined) { |
| nextDefaults.contextPruning = { |
| ...contextPruning, |
| mode: "cache-ttl", |
| ttl: defaults.contextPruning?.ttl ?? "1h", |
| }; |
| mutated = true; |
| } |
|
|
| if (defaults.heartbeat?.every === undefined) { |
| nextDefaults.heartbeat = { |
| ...heartbeat, |
| every: authMode === "oauth" ? "1h" : "30m", |
| }; |
| mutated = true; |
| } |
|
|
| if (authMode === "api_key") { |
| const nextModels = defaults.models ? { ...defaults.models } : {}; |
| let modelsMutated = false; |
|
|
| for (const [key, entry] of Object.entries(nextModels)) { |
| const parsed = parseModelRef(key, "anthropic"); |
| if (!parsed || parsed.provider !== "anthropic") { |
| continue; |
| } |
| const current = entry ?? {}; |
| const params = (current as { params?: Record<string, unknown> }).params ?? {}; |
| if (typeof params.cacheControlTtl === "string") { |
| continue; |
| } |
| nextModels[key] = { |
| ...(current as Record<string, unknown>), |
| params: { ...params, cacheControlTtl: "1h" }, |
| }; |
| modelsMutated = true; |
| } |
|
|
| const primary = resolvePrimaryModelRef(defaults.model?.primary ?? undefined); |
| if (primary) { |
| const parsedPrimary = parseModelRef(primary, "anthropic"); |
| if (parsedPrimary?.provider === "anthropic") { |
| const key = `${parsedPrimary.provider}/${parsedPrimary.model}`; |
| const entry = nextModels[key]; |
| const current = entry ?? {}; |
| const params = (current as { params?: Record<string, unknown> }).params ?? {}; |
| if (typeof params.cacheControlTtl !== "string") { |
| nextModels[key] = { |
| ...(current as Record<string, unknown>), |
| params: { ...params, cacheControlTtl: "1h" }, |
| }; |
| modelsMutated = true; |
| } |
| } |
| } |
|
|
| if (modelsMutated) { |
| nextDefaults.models = nextModels; |
| mutated = true; |
| } |
| } |
|
|
| if (!mutated) { |
| return cfg; |
| } |
|
|
| return { |
| ...cfg, |
| agents: { |
| ...cfg.agents, |
| defaults: nextDefaults, |
| }, |
| }; |
| } |
|
|
| export function applyCompactionDefaults(cfg: OpenClawConfig): OpenClawConfig { |
| const defaults = cfg.agents?.defaults; |
| if (!defaults) { |
| return cfg; |
| } |
| const compaction = defaults?.compaction; |
| if (compaction?.mode) { |
| return cfg; |
| } |
|
|
| return { |
| ...cfg, |
| agents: { |
| ...cfg.agents, |
| defaults: { |
| ...defaults, |
| compaction: { |
| ...compaction, |
| mode: "safeguard", |
| }, |
| }, |
| }, |
| }; |
| } |
|
|
| export function resetSessionDefaultsWarningForTests() { |
| defaultWarnState = { warned: false }; |
| } |
|
|