import type { StreamFn } from "@mariozechner/pi-agent-core"; import type { SimpleStreamOptions } from "@mariozechner/pi-ai"; import { streamSimple } from "@mariozechner/pi-ai"; import type { OpenClawConfig } from "../../config/config.js"; import { log } from "./logger.js"; const OPENROUTER_APP_HEADERS: Record = { "HTTP-Referer": "https://openclaw.ai", "X-Title": "OpenClaw", }; /** * Resolve provider-specific extra params from model config. * Used to pass through stream params like temperature/maxTokens. * * @internal Exported for testing only */ export function resolveExtraParams(params: { cfg: OpenClawConfig | undefined; provider: string; modelId: string; }): Record | undefined { const modelKey = `${params.provider}/${params.modelId}`; const modelConfig = params.cfg?.agents?.defaults?.models?.[modelKey]; return modelConfig?.params ? { ...modelConfig.params } : undefined; } type CacheRetention = "none" | "short" | "long"; type CacheRetentionStreamOptions = Partial & { cacheRetention?: CacheRetention; }; /** * Resolve cacheRetention from extraParams, supporting both new `cacheRetention` * and legacy `cacheControlTtl` values for backwards compatibility. * * Mapping: "5m" → "short", "1h" → "long" * * Only applies to Anthropic provider (OpenRouter uses openai-completions API * with hardcoded cache_control, not the cacheRetention stream option). */ function resolveCacheRetention( extraParams: Record | undefined, provider: string, ): CacheRetention | undefined { if (provider !== "anthropic") { return undefined; } // Prefer new cacheRetention if present const newVal = extraParams?.cacheRetention; if (newVal === "none" || newVal === "short" || newVal === "long") { return newVal; } // Fall back to legacy cacheControlTtl with mapping const legacy = extraParams?.cacheControlTtl; if (legacy === "5m") { return "short"; } if (legacy === "1h") { return "long"; } return undefined; } function createStreamFnWithExtraParams( baseStreamFn: StreamFn | undefined, extraParams: Record | undefined, provider: string, ): StreamFn | undefined { if (!extraParams || Object.keys(extraParams).length === 0) { return undefined; } const streamParams: CacheRetentionStreamOptions = {}; if (typeof extraParams.temperature === "number") { streamParams.temperature = extraParams.temperature; } if (typeof extraParams.maxTokens === "number") { streamParams.maxTokens = extraParams.maxTokens; } const cacheRetention = resolveCacheRetention(extraParams, provider); if (cacheRetention) { streamParams.cacheRetention = cacheRetention; } if (Object.keys(streamParams).length === 0) { return undefined; } log.debug(`creating streamFn wrapper with params: ${JSON.stringify(streamParams)}`); const underlying = baseStreamFn ?? streamSimple; const wrappedStreamFn: StreamFn = (model, context, options) => underlying(model, context, { ...streamParams, ...options, }); return wrappedStreamFn; } /** * Create a streamFn wrapper that adds OpenRouter app attribution headers. * These headers allow OpenClaw to appear on OpenRouter's leaderboard. */ function createOpenRouterHeadersWrapper(baseStreamFn: StreamFn | undefined): StreamFn { const underlying = baseStreamFn ?? streamSimple; return (model, context, options) => underlying(model, context, { ...options, headers: { ...OPENROUTER_APP_HEADERS, ...options?.headers, }, }); } /** * Apply extra params (like temperature) to an agent's streamFn. * Also adds OpenRouter app attribution headers when using the OpenRouter provider. * * @internal Exported for testing */ export function applyExtraParamsToAgent( agent: { streamFn?: StreamFn }, cfg: OpenClawConfig | undefined, provider: string, modelId: string, extraParamsOverride?: Record, ): void { const extraParams = resolveExtraParams({ cfg, provider, modelId, }); const override = extraParamsOverride && Object.keys(extraParamsOverride).length > 0 ? Object.fromEntries( Object.entries(extraParamsOverride).filter(([, value]) => value !== undefined), ) : undefined; const merged = Object.assign({}, extraParams, override); const wrappedStreamFn = createStreamFnWithExtraParams(agent.streamFn, merged, provider); if (wrappedStreamFn) { log.debug(`applying extraParams to agent streamFn for ${provider}/${modelId}`); agent.streamFn = wrappedStreamFn; } if (provider === "openrouter") { log.debug(`applying OpenRouter app attribution headers for ${provider}/${modelId}`); agent.streamFn = createOpenRouterHeadersWrapper(agent.streamFn); } }