diff --git a/src/cost/pricing-fetcher.ts b/src/cost/pricing-fetcher.ts index c10fcb4b0..8e78fd15f 100644 --- a/src/cost/pricing-fetcher.ts +++ b/src/cost/pricing-fetcher.ts @@ -2,8 +2,9 @@ import { mkdir } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; +import { getLogger } from "@intx/log"; import { type } from "arktype"; -import { SETTINGS_DIR_NAME } from "../branding.js"; +import { LOG_NAMESPACE_ROOT, SETTINGS_DIR_NAME } from "../branding.js"; export interface ModelPricing { inputPricePerToken: number; @@ -47,6 +48,8 @@ const DEFAULT_ENDPOINT = "https://models.dev/api.json"; const DEFAULT_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000; const DEFAULT_FETCH_TIMEOUT_MS = 5000; +const log = getLogger([LOG_NAMESPACE_ROOT, "cost", "pricing"]); + const modelPricingValidator = type({ inputPricePerToken: "number", outputPricePerToken: "number", @@ -288,7 +291,11 @@ export async function loadPricing( const pricing = await fetchPricing(options); await writePricingCache(pricing, cachePath); return pricing; - } catch { + } catch (err) { + // One debug line keeps a failed refresh observable while stale cache carries on. + log.debug("pricing refresh failed, keeping cached pricing: {error}", { + error: err instanceof Error ? err.message : String(err), + }); return readPricingCache(cachePath); } } diff --git a/src/pricing-fetcher.test.ts b/src/pricing-fetcher.test.ts index af52a0132..c4db4414b 100644 --- a/src/pricing-fetcher.test.ts +++ b/src/pricing-fetcher.test.ts @@ -1,6 +1,7 @@ import { defined } from "../testkit/defined.js"; import { describe, test, expect } from "bun:test"; import { isAbsolute, join } from "node:path"; +import { configureSync, getConfig, resetSync } from "@intx/log"; import { parseModelsDevContextWindows, parseModelsDevPricing, @@ -259,6 +260,43 @@ describe("loadPricing", () => { expect(result).toBeNull(); }); + + test("emits a single debug line when refresh fails", async () => { + const records: { level: string; text: string }[] = []; + const previous = getConfig(); + configureSync({ + reset: true, + sinks: { + test: (record) => { + records.push({ + level: record.level, + text: record.message.map(String).join(""), + }); + }, + }, + loggers: [{ category: [], lowestLevel: "debug", sinks: ["test"] }], + }); + try { + const failingFetch = (async (): Promise => { + throw new Error("network error"); + }) as unknown as typeof fetch; + const result = await loadPricing({ + fetchImpl: failingFetch, + cachePath: "/tmp/nonexistent-pricing-cache-debug.json", + }); + expect(result).toBeNull(); + const hits = records.filter( + (record) => + record.level === "debug" && + record.text.includes("pricing refresh failed"), + ); + expect(hits).toHaveLength(1); + expect(hits[0]?.text).toContain("network error"); + } finally { + if (previous === null) resetSync(); + else configureSync({ ...previous, reset: true }); + } + }); }); // ---------------------------------------------------------------------------