// Provides model selection, usage, and thinking-level utility helpers. import { resolveClaudeFable5ModelIdentity, resolveClaudeNativeThinkingLevelMap, } from "@openclaw/llm-core"; import type { Api, Model, ModelThinkingLevel, Usage } from "./types.js"; /** Calculates and stores model cost fields from token usage and per-million pricing. */ export function calculateCost(model: Model, usage: Usage): Usage["cost"] { usage.cost.input = (model.cost.input / 1000000) * usage.input; usage.cost.output = (model.cost.output / 1000000) * usage.output; usage.cost.cacheRead = (model.cost.cacheRead / 1000000) * usage.cacheRead; usage.cost.cacheWrite = (model.cost.cacheWrite / 1000000) * usage.cacheWrite; usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite; return usage.cost; } const EXTENDED_THINKING_LEVELS: ModelThinkingLevel[] = [ "off", "minimal", "low", "medium", "high", "xhigh", "max", ]; function resolveThinkingLevelMap(model: Model) { return model.api === "anthropic-messages" ? (resolveClaudeNativeThinkingLevelMap(model) ?? model.thinkingLevelMap) : model.thinkingLevelMap; } /** Returns thinking levels exposed by a reasoning-capable model. */ export function getSupportedThinkingLevels( model: Model, ): ModelThinkingLevel[] { const fableContract = model.api === "anthropic-messages" && resolveClaudeFable5ModelIdentity(model) !== undefined; if (!model.reasoning && !fableContract) { return ["off"]; } const thinkingLevelMap = resolveThinkingLevelMap(model); return EXTENDED_THINKING_LEVELS.filter((level) => { const mapped = thinkingLevelMap?.[level]; if (mapped === null) { return false; } if (level === "xhigh" || level === "max") { return mapped !== undefined; } return true; }); } /** Clamps a requested thinking level to the closest supported level for a model. */ export function clampThinkingLevel( model: Model, level: ModelThinkingLevel, ): ModelThinkingLevel { const availableLevels = getSupportedThinkingLevels(model); if (availableLevels.includes(level)) { return level; } const requestedIndex = EXTENDED_THINKING_LEVELS.indexOf(level); if (requestedIndex === -1) { return availableLevels[0] ?? "off"; } // Explicit provider opt-outs are hard caps. Downgrade them before considering // stronger levels so unsupported xhigh/max requests cannot increase cost. const thinkingLevelMap = resolveThinkingLevelMap(model); if ((level === "xhigh" || level === "max") && thinkingLevelMap?.[level] === null) { for (let i = requestedIndex - 1; i >= 0; i--) { const candidate = EXTENDED_THINKING_LEVELS[i]; if (availableLevels.includes(candidate)) { return candidate; } } } // Prefer the next stronger available level, then walk down if the request was above the model cap. for (let i = requestedIndex; i < EXTENDED_THINKING_LEVELS.length; i++) { const candidate = EXTENDED_THINKING_LEVELS[i]; if (availableLevels.includes(candidate)) { return candidate; } } for (let i = requestedIndex - 1; i >= 0; i--) { const candidate = EXTENDED_THINKING_LEVELS[i]; if (availableLevels.includes(candidate)) { return candidate; } } return availableLevels[0] ?? "off"; } /** Compares model identity by provider and id. */ export function modelsAreEqual( a: Model | null | undefined, b: Model | null | undefined, ): boolean { if (!a || !b) { return false; } return a.id === b.id && a.provider === b.provider; }