fix(opencode-plugin): report disk-cache age in the stale-fallback warning (#13426)

Merged after boarding with #13185 into one worktree cut from `release/v3.8.51` (both verified as ancestors of the combined HEAD before validating).

**Evidence**
- Full `@omniroute/opencode-plugin` suite — not just this PR's file: **373/373 pass** across all 26 test files.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS (2842 vs baseline 3218), `check-cognitive-complexity` PASS (1284 vs 1437), `typecheck:core` PASS.
- `check-file-size`: no violation attributable to this PR.

**One note for the record.** On a first run the suite reported `scaffold: built ESM default export resolves with the v1 plugin shape` failing with `ERR_MODULE_NOT_FOUND … dist/index.js`. That was a fresh worktree without the plugin built, not a defect in this PR — after `npm run build` in the workspace, all 373 pass. Flagging it because that failure reads exactly like a PR bug and could easily be misattributed to you on a future run.

Thanks, @RaviTharuma — putting the snapshot age in the warning turns "using stale disk cache" from a fact into something an operator can act on, and you covered `snapshotAgeLabel` with both the numeric-`writtenAt` and missing-`writtenAt` cases rather than only the happy path.
This commit is contained in:
Ravi Tharuma
2026-09-17 03:56:38 +02:00
committed by GitHub
parent 5acac8021d
commit 683a552fcb
3 changed files with 101 additions and 34 deletions

View File

@@ -5556,9 +5556,17 @@ export function createOmniRouteConfigHook(
if (modelsFetchThrew && wantDiskCache && !warmSnapshot) {
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
if (snapshot && snapshot.rawModels.length > 0) {
// Report snapshot age like the warm-startup path already does:
// "stale" alone reads as a transient blip, so a week-old catalog
// is indistinguishable from a five-minute-old one.
const snapshotAge = snapshot.writtenAt;
const snapshotAgeLabel =
typeof snapshotAge === "number"
? `${Math.round((Date.now() - snapshotAge) / 3_600_000)}h`
: "unknown";
logAt(
"warn",
`config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
`config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models, age ${snapshotAgeLabel})`
);
localRawModels = snapshot.rawModels;
localRawCombos = snapshot.rawCombos;

View File

@@ -481,10 +481,7 @@ test("config: combos fetcher throws → emit models-only catalog (no combos in m
];
assert.ok(entry);
const ids = Object.keys(entry.models).sort();
assert.deepEqual(ids, [
"claude-sonnet-4-6",
"gemini-3-flash",
]);
assert.deepEqual(ids, ["claude-sonnet-4-6", "gemini-3-flash"]);
assert.equal(entry.models["claude-tier"], undefined, "no combo entry");
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
@@ -1041,11 +1038,7 @@ test("config: features.enrichment=false skips enrichment fetch + keeps raw-id na
];
assert.ok(entry);
assert.equal(enrichmentFetcher.callCount(), 0, "enrichment fetch suppressed by feature flag");
assert.equal(
entry.models["claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id retained"
);
assert.equal(entry.models["claude-sonnet-4-6"].name, "claude-sonnet-4-6", "raw id retained");
});
test("config: enrichment fetcher throws → soft-fail (warn + raw-id static catalog)", async () => {
@@ -1068,11 +1061,7 @@ test("config: enrichment fetcher throws → soft-fail (warn + raw-id static cata
"opencode-omniroute"
];
assert.ok(entry, "static block still published on enrichment failure");
assert.equal(
entry.models["claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id retained"
);
assert.equal(entry.models["claude-sonnet-4-6"].name, "claude-sonnet-4-6", "raw id retained");
assert.equal(enrichmentFetcher.callCount(), 1);
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/pricing/models fetch failed")),
@@ -1270,10 +1259,7 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(
entry.models["claude-sonnet-4-6"],
"stale snapshot hydrated into static block"
);
assert.ok(entry.models["claude-sonnet-4-6"], "stale snapshot hydrated into static block");
assert.equal(
entry.models["claude-sonnet-4-6"].name,
"Claude Sonnet 4.6 (cached)",
@@ -1281,14 +1267,95 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
);
assert.equal(writes, 0, "disk write skipped when live fetch failed");
assert.ok(
logger.entries.some((e) =>
String(e[0]).includes("using stale disk cache") ||
String(e[0]).includes("warm startup from disk snapshot")
logger.entries.some(
(e) =>
String(e[0]).includes("using stale disk cache") ||
String(e[0]).includes("warm startup from disk snapshot")
),
"disk-cache hydration breadcrumb emitted"
);
});
// The stale-fallback branch (`modelsFetchThrew && wantDiskCache && !warmSnapshot`)
// only runs when the warm-startup read found nothing — a snapshot can appear on
// disk between that first read and the live fetch failing (e.g. another OC
// process instance wrote one concurrently). A stateful reader simulates that:
// empty on the warm-startup read, populated by the time the fallback re-reads.
function emptyThenSnapshotReader(
snapshot: Omit<
Awaited<ReturnType<typeof import("../src/index.js").defaultDiskSnapshotReader>> & object,
never
>
): typeof import("../src/index.js").defaultDiskSnapshotReader {
let calls = 0;
return (async () => {
calls++;
return calls === 1 ? undefined : snapshot;
}) as typeof import("../src/index.js").defaultDiskSnapshotReader;
}
test("config: stale-fallback warning reports the disk snapshot age in hours", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = throwingModelsFetcher();
const combosFetcher = stubCombosFetcher([]);
const logger = captureWarn();
const writtenAt = Date.now() - 2 * 3_600_000; // 2h old
const diskSnapshotReader = emptyThenSnapshotReader({
rawModels: [MODEL_CLAUDE],
rawCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
writtenAt,
});
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute", features: { diskCache: true } },
{ readAuthJson, fetcher, combosFetcher, diskSnapshotReader, logger }
);
await hook(makeInput());
assert.ok(
logger.entries.some((e) => String(e[0]).includes("using stale disk cache (1 models, age 2h)")),
"stale-fallback warning includes the computed snapshot age"
);
});
test('config: stale-fallback warning falls back to "unknown" age without writtenAt', async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = throwingModelsFetcher();
const combosFetcher = stubCombosFetcher([]);
const logger = captureWarn();
const diskSnapshotReader = emptyThenSnapshotReader({
rawModels: [MODEL_CLAUDE],
rawCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
});
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute", features: { diskCache: true } },
{ readAuthJson, fetcher, combosFetcher, diskSnapshotReader, logger }
);
await hook(makeInput());
assert.ok(
logger.entries.some((e) =>
String(e[0]).includes("using stale disk cache (1 models, age unknown)")
),
'stale-fallback warning falls back to "unknown" when writtenAt is absent'
);
});
test("config: cached rawEnrichment from earlier provider hook is reused (no refetch)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-shared", baseURL: "https://or.example/v1" },
@@ -1376,10 +1443,7 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
"opencode-omniroute"
];
assert.ok(entry);
assert.equal(
entry.models["claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
assert.equal(entry.models["gemini-3-flash"].name, "Gemini - Gemini 3 Flash");
// Combos stay untouched — `Combo: ` prefix already conveys multi-upstream.
assert.equal(entry.models["claude-tier"].name, "Claude Tier");
@@ -1495,10 +1559,7 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
const entryA = (inputA as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(
entryA.models["claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
assert.equal(entryA.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
// Second invocation (cache hit) — name must still be single-suffixed.
const inputB = makeInput();
@@ -1506,10 +1567,7 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
const entryB = (inputB as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(
entryB.models["claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
assert.equal(entryB.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
});
// ────────────────────────────────────────────────────────────────────────────

View File

@@ -0,0 +1 @@
- **fix(opencode-plugin):** The stale disk-cache fallback warning now reports the snapshot's age (`using stale disk cache (N models, age 168h)`), matching the existing warm-startup log. Previously a week-old catalog was indistinguishable from a five-minute-old one, so silent model drift went unnoticed. (#13426)