// Provides model selection, usage, and thinking-level utility helpers. import { resolveClaudeNativeThinkingLevelMap, requiresClaudeMandatoryAdaptiveThinking, } 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; } /** Replaces the catalog estimate when the provider reports an authoritative billed total. */ export function applyProviderReportedUsageCost(usage: Usage, reportedCost: unknown): void { if (typeof reportedCost !== "number" || !Number.isFinite(reportedCost) || reportedCost < 0) { return; } usage.cost.total = reportedCost; usage.cost.totalOrigin = "provider-billed"; } 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 mandatoryAdaptiveContract = model.api === "anthropic-messages" && requiresClaudeMandatoryAdaptiveThinking(model); if (!model.reasoning && !mandatoryAdaptiveContract) { 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 (const candidate of EXTENDED_THINKING_LEVELS.slice(0, requestedIndex).toReversed()) { if (availableLevels.includes(candidate)) { return candidate; } } } // Prefer the next stronger available level, then walk down if the request was above the model cap. for (const candidate of EXTENDED_THINKING_LEVELS.slice(requestedIndex)) { if (availableLevels.includes(candidate)) { return candidate; } } for (const candidate of EXTENDED_THINKING_LEVELS.slice(0, requestedIndex).toReversed()) { 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; }