mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-26 06:51:18 +00:00
* fix(usage): preserve provider-billed zero totals * fix(usage): harden provider-billed cost provenance * fix(openrouter): retry delayed generation metadata * fix(openrouter): satisfy retry lint * refactor(openrouter): consume streamed billed cost * chore: keep release notes out of contributor PR --------- Co-authored-by: snowzlmbot <293528334+snowzlmbot@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
120 lines
4.0 KiB
TypeScript
120 lines
4.0 KiB
TypeScript
// 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<TApi extends Api>(model: Model<TApi>, 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<TApi extends Api>(model: Model<TApi>) {
|
|
return model.api === "anthropic-messages"
|
|
? (resolveClaudeNativeThinkingLevelMap(model) ?? model.thinkingLevelMap)
|
|
: model.thinkingLevelMap;
|
|
}
|
|
|
|
/** Returns thinking levels exposed by a reasoning-capable model. */
|
|
export function getSupportedThinkingLevels<TApi extends Api>(
|
|
model: Model<TApi>,
|
|
): 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<TApi extends Api>(
|
|
model: Model<TApi>,
|
|
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<TApi extends Api>(
|
|
a: Model<TApi> | null | undefined,
|
|
b: Model<TApi> | null | undefined,
|
|
): boolean {
|
|
if (!a || !b) {
|
|
return false;
|
|
}
|
|
return a.id === b.id && a.provider === b.provider;
|
|
}
|