fix: log pricing fetch timeout duration

This commit is contained in:
Peter Steinberger
2026-04-21 05:11:53 +01:00
parent 2641b052dc
commit 5986431b02
2 changed files with 70 additions and 2 deletions

View File

@@ -1,6 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { modelKey } from "../agents/model-selection.js";
import type { OpenClawConfig } from "../config/config.js";
import { resetLogger, setLoggerOverride } from "../logging/logger.js";
import { loggingState } from "../logging/state.js";
import type { normalizeProviderModelIdWithPlugin } from "../plugins/provider-runtime.js";
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
@@ -26,6 +28,8 @@ describe("model-pricing-cache", () => {
afterEach(() => {
__resetGatewayModelPricingCacheForTest();
loggingState.rawConsole = null;
resetLogger();
});
it("collects configured model refs across defaults, aliases, overrides, and media tools", () => {
@@ -515,6 +519,45 @@ describe("model-pricing-cache", () => {
});
});
it("logs configured timeout seconds when pricing fetches time out", async () => {
const warnings: string[] = [];
loggingState.rawConsole = {
log: vi.fn(),
info: vi.fn(),
warn: vi.fn((message: string) => warnings.push(message)),
error: vi.fn(),
};
setLoggerOverride({ level: "silent", consoleLevel: "warn" });
const config = {
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-6" },
},
},
} as unknown as OpenClawConfig;
const timeoutError = new DOMException(
"The operation was aborted due to timeout",
"TimeoutError",
);
const fetchImpl = withFetchPreconnect(async () => {
throw timeoutError;
});
await refreshGatewayModelPricingCache({ config, fetchImpl });
expect(warnings).toEqual(
expect.arrayContaining([
expect.stringContaining(
"OpenRouter pricing fetch failed (timeout 15s): TimeoutError: The operation was aborted due to timeout",
),
expect.stringContaining(
"LiteLLM pricing fetch failed (timeout 15s): TimeoutError: The operation was aborted due to timeout",
),
]),
);
});
it("treats oversized LiteLLM catalog responses as source failures", async () => {
const config = {
agents: {

View File

@@ -98,6 +98,31 @@ function parseNumberString(value: unknown): number | null {
return Number.isFinite(parsed) ? parsed : null;
}
function formatTimeoutSeconds(timeoutMs: number): string {
const seconds = timeoutMs / 1000;
return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`;
}
function readErrorName(error: unknown): string | undefined {
return error && typeof error === "object" && "name" in error
? String((error as { name?: unknown }).name)
: undefined;
}
function isTimeoutError(error: unknown): boolean {
if (readErrorName(error) === "TimeoutError") {
return true;
}
return /\bTimeoutError\b/u.test(String(error));
}
function formatPricingFetchFailure(source: "LiteLLM" | "OpenRouter", error: unknown): string {
if (isTimeoutError(error)) {
return `${source} pricing fetch failed (timeout ${formatTimeoutSeconds(FETCH_TIMEOUT_MS)}): ${String(error)}`;
}
return `${source} pricing fetch failed: ${String(error)}`;
}
function toPricePerMillion(value: number | null): number {
if (value === null || value < 0 || !Number.isFinite(value)) {
return 0;
@@ -535,12 +560,12 @@ export async function refreshGatewayModelPricingCache(params: {
let litellmFailed = false;
const [catalogById, litellmCatalog] = await Promise.all([
fetchOpenRouterPricingCatalog(fetchImpl).catch((error: unknown) => {
log.warn(`OpenRouter pricing fetch failed: ${String(error)}`);
log.warn(formatPricingFetchFailure("OpenRouter", error));
openRouterFailed = true;
return new Map<string, OpenRouterPricingEntry>();
}),
fetchLiteLLMPricingCatalog(fetchImpl).catch((error: unknown) => {
log.warn(`LiteLLM pricing fetch failed: ${String(error)}`);
log.warn(formatPricingFetchFailure("LiteLLM", error));
litellmFailed = true;
return new Map<string, CachedModelPricing>() as LiteLLMPricingCatalog;
}),