Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/18507-version-lookup-retry-window.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(providers):** Stop the Copilot, Claude Code, Gemini, Qwen, CodeBuddy, Kimi and Zed client-version getters from repeating their npm/GitHub lookup on every call when the registry answers the pinned version (their cache only kept newer versions), and stop all of them, OpenCode included, from retrying a failed lookup on every request: an answer now counts as checked for the existing 6-hour TTL, and a failure is retried after 15 minutes ([#18507](https://github.com/diegosouzapw/OmniRoute/pull/18507))
41 changes: 37 additions & 4 deletions open-sse/config/providerHeaderProfiles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { getAntigravityContentHeaders } from "../services/antigravityHeaders.ts";
import type { AntigravityClientProfile } from "@/shared/constants/antigravityClientProfile";
import { canStartBackgroundRefresh } from "@/shared/utils/backgroundRefresh";
import {
BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS,
canStartBackgroundRefresh,
} from "@/shared/utils/backgroundRefresh";

// GitHub Copilot request identity. Ported to match the GitHub Copilot CLI
// (`copilot` npm package) wire identity that Hermes captured live, NOT the
Expand All @@ -24,6 +27,9 @@ type CopilotFetchLike = typeof fetch;

let copilotVersionCache: { fetchedAt: number; version: string } | null = null;
let copilotVersionInFlight: Promise<string> | null = null;
// Earliest time the getter may start another background lookup: one TTL after any answer
// (the cache only keeps versions newer than the pin), the retry window after a failure.
let copilotNextRefreshAt = 0;

function getSafeCopilotEnvValue(name: string, pattern: RegExp): string | null {
const raw = typeof process === "undefined" ? undefined : process.env?.[name];
Expand Down Expand Up @@ -103,6 +109,9 @@ export function resolveGitHubCopilotCliVersion(
if (resolved && compareCopilotDottedTriple(resolved, GITHUB_COPILOT_CLI_VERSION) > 0) {
copilotVersionCache = { fetchedAt: Date.now(), version: resolved };
}
copilotNextRefreshAt =
Date.now() +
(resolved ? GITHUB_COPILOT_VERSION_CACHE_TTL_MS : BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS);
return pickCopilotVersionAtLeastPin(resolved ?? readFreshCopilotVersionCache());
})();

Expand All @@ -115,6 +124,7 @@ export function resolveGitHubCopilotCliVersion(

/** Test seam: drop the registry cache so the next resolve hits the network. */
export function resetGitHubCopilotCliVersionCache(): void {
copilotNextRefreshAt = 0;
copilotVersionCache = null;
copilotVersionInFlight = null;
}
Expand All @@ -130,7 +140,8 @@ export function getGitHubCopilotCliVersion(): string {
canStartBackgroundRefresh() &&
!process.env.NODE_TEST_CONTEXT &&
!readFreshCopilotVersionCache() &&
!copilotVersionInFlight
!copilotVersionInFlight &&
Date.now() >= copilotNextRefreshAt
)
void resolveGitHubCopilotCliVersion();
return pickCopilotVersionAtLeastPin(readFreshCopilotVersionCache());
Expand Down Expand Up @@ -242,6 +253,9 @@ let qwenVersionCache: VersionCache | null = null;
let qwenVersionInFlight: Promise<string> | null = null;
let codeBuddyVersionCache: VersionCache | null = null;
let codeBuddyVersionInFlight: Promise<string> | null = null;
// Earliest time each getter may start another background lookup (see refreshNpmDottedTriple).
let qwenNextRefreshAt = 0;
let codeBuddyNextRefreshAt = 0;

function parseDottedTriple(value: unknown): string | null {
if (typeof value !== "string") return null;
Expand Down Expand Up @@ -288,6 +302,8 @@ function refreshNpmDottedTriple(options: {
setCache: (next: VersionCache) => void;
readInFlight: () => Promise<string> | null;
setInFlight: (next: Promise<string> | null) => void;
/** Records when a getter may start the next background lookup. */
setNextRefreshAt: (at: number) => void;
fetchImpl: NpmFetchLike;
}): Promise<string> {
const fresh = readFreshVersionCache(options.readCache(), options.ttlMs);
Expand Down Expand Up @@ -317,6 +333,11 @@ function refreshNpmDottedTriple(options: {
clearTimeout(timeoutId);
}

// Any answer counts as checked for the TTL — the cache only keeps newer-than-pin versions,
// so an answer equal to the pin would otherwise be re-fetched on every call.
options.setNextRefreshAt(
Date.now() + (resolved ? options.ttlMs : BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS)
);
if (resolved && compareDottedTriple(resolved, options.pin) > 0) {
options.setCache({ fetchedAt: Date.now(), version: resolved });
return resolved;
Expand All @@ -335,7 +356,7 @@ function refreshNpmDottedTriple(options: {

/** Sync hot path. Cache if newer than the pin, else the pin. Never waits. */
export function getQwenCliVersion(): string {
if (shouldAutoRefreshCliVersion() && !qwenVersionInFlight) {
if (shouldAutoRefreshCliVersion() && !qwenVersionInFlight && Date.now() >= qwenNextRefreshAt) {
const fresh = readFreshVersionCache(qwenVersionCache, QWEN_CLI_VERSION_CACHE_TTL_MS);
if (!fresh || compareDottedTriple(fresh, QWEN_CLI_VERSION) <= 0) {
void refreshQwenCliVersion();
Expand All @@ -361,11 +382,15 @@ export function refreshQwenCliVersion(fetchImpl: NpmFetchLike = fetch): Promise<
setInFlight: (next) => {
qwenVersionInFlight = next;
},
setNextRefreshAt: (at) => {
qwenNextRefreshAt = at;
},
fetchImpl,
});
}

export function resetQwenCliVersionCache(): void {
qwenNextRefreshAt = 0;
qwenVersionCache = null;
qwenVersionInFlight = null;
}
Expand All @@ -376,7 +401,11 @@ function formatCodeBuddyCnUserAgent(version: string): string {

/** Sync hot path. Both numbers move together, or the captured pin stays. */
export function getCodeBuddyCnUserAgent(): string {
if (shouldAutoRefreshCliVersion() && !codeBuddyVersionInFlight) {
if (
shouldAutoRefreshCliVersion() &&
!codeBuddyVersionInFlight &&
Date.now() >= codeBuddyNextRefreshAt
) {
const fresh = readFreshVersionCache(codeBuddyVersionCache, CODEBUDDY_CN_VERSION_CACHE_TTL_MS);
if (!fresh) void refreshCodeBuddyCnUserAgent();
}
Expand All @@ -401,11 +430,15 @@ export function refreshCodeBuddyCnUserAgent(fetchImpl: NpmFetchLike = fetch): Pr
setInFlight: (next) => {
codeBuddyVersionInFlight = next;
},
setNextRefreshAt: (at) => {
codeBuddyNextRefreshAt = at;
},
fetchImpl,
}).then((version) => formatCodeBuddyCnUserAgent(version));
}

export function resetCodeBuddyCnVersionCache(): void {
codeBuddyNextRefreshAt = 0;
codeBuddyVersionCache = null;
codeBuddyVersionInFlight = null;
}
Expand Down
15 changes: 13 additions & 2 deletions open-sse/config/providers/registry/kimi/coding/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { canStartBackgroundRefresh } from "@/shared/utils/backgroundRefresh";
import {
BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS,
canStartBackgroundRefresh,
} from "@/shared/utils/backgroundRefresh";

export const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/v1";
export const KIMI_CODING_MODELS_URL = `${KIMI_CODING_BASE_URL}/models`;
Expand Down Expand Up @@ -81,6 +84,9 @@ type FetchLike = typeof fetch;
let cachedVersion: string | null = null;
let cachedAt = 0;
let inFlight: Promise<string> | null = null;
// Earliest time the getter may start another background lookup: one TTL after any answer
// (the cache below only keeps versions newer than the pin), the retry window after a failure.
let nextBackgroundRefreshAt = 0;
let fetchImpl: FetchLike = fetch;

function parseDottedTriple(value: unknown): string | null {
Expand Down Expand Up @@ -129,7 +135,8 @@ export function getKimiCodeCliVersion(): string {
canStartBackgroundRefresh() &&
!process.env.NODE_TEST_CONTEXT &&
!readFreshCache() &&
!inFlight
!inFlight &&
Date.now() >= nextBackgroundRefreshAt
) {
void refreshKimiCodeCliVersion();
}
Expand Down Expand Up @@ -178,6 +185,9 @@ export function refreshKimiCodeCliVersion(): Promise<string> {
cachedVersion = resolved;
cachedAt = Date.now();
}
nextBackgroundRefreshAt =
Date.now() +
(resolved ? KIMI_CODE_VERSION_CACHE_TTL_MS : BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS);
return pickAtLeastPin(resolved ?? readFreshCache());
})();

Expand All @@ -190,6 +200,7 @@ export function refreshKimiCodeCliVersion(): Promise<string> {

/** Test seam: drop the registry cache so the next refresh hits the network. */
export function resetKimiCodeCliVersionCache(): void {
nextBackgroundRefreshAt = 0;
cachedVersion = null;
cachedAt = 0;
inFlight = null;
Expand Down
15 changes: 13 additions & 2 deletions open-sse/executors/zedClientVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
* executor config.appVersion still wins over both.
*/

import { canStartBackgroundRefresh } from "@/shared/utils/backgroundRefresh";
import {
BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS,
canStartBackgroundRefresh,
} from "@/shared/utils/backgroundRefresh";

export const ZED_CLIENT_VERSION = "0.200.0";

Expand All @@ -21,6 +24,9 @@ type FetchLike = typeof fetch;
let cachedVersion: string | null = null;
let cachedAt = 0;
let inFlight: Promise<string> | null = null;
// Earliest time the getter may start another background lookup: one TTL after any answer
// (the cache below only keeps versions newer than the pin), the retry window after a failure.
let nextBackgroundRefreshAt = 0;

function parseReleaseTag(tagName: unknown): string | null {
if (typeof tagName !== "string") return null;
Expand Down Expand Up @@ -60,7 +66,8 @@ export function getZedClientVersion(): string {
canStartBackgroundRefresh() &&
!process.env.NODE_TEST_CONTEXT &&
!readFreshCache() &&
!inFlight
!inFlight &&
Date.now() >= nextBackgroundRefreshAt
) {
void refreshZedClientVersion();
}
Expand Down Expand Up @@ -104,6 +111,9 @@ export function refreshZedClientVersion(fetchImpl: FetchLike = fetch): Promise<s
cachedVersion = resolved;
cachedAt = Date.now();
}
nextBackgroundRefreshAt =
Date.now() +
(resolved ? ZED_VERSION_CACHE_TTL_MS : BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS);
return pickAtLeastPin(resolved ?? readFreshCache());
})();

Expand All @@ -116,6 +126,7 @@ export function refreshZedClientVersion(fetchImpl: FetchLike = fetch): Promise<s

/** Test seam: drop the release cache so the next refresh hits the network. */
export function resetZedClientVersionCache(): void {
nextBackgroundRefreshAt = 0;
cachedVersion = null;
cachedAt = 0;
inFlight = null;
Expand Down
12 changes: 12 additions & 0 deletions open-sse/utils/opencodeCliVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* cache in background and later requests send the live version.
*/

import { BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS } from "@/shared/utils/backgroundRefresh";

const NPM_OPENCODE_LATEST_URL = "https://registry.npmjs.org/opencode-ai/latest";

/** Cold/offline fallback. Bump when the pin visibly lags the registry. */
Expand All @@ -36,6 +38,9 @@ const FETCH_TIMEOUT_MS = 5_000;
let cachedVersion: string | null = null;
let cachedAt = 0;
let inFlight: Promise<string> | null = null;
// A failed lookup caches nothing and this refresh runs on every request, so a failure is
// retried only after the shared retry window.
let retryAfterFailureAt = 0;

export function isOpencodeCliVersion(value: unknown): value is string {
return typeof value === "string" && /^\d+\.\d+\.\d+$/.test(value.trim());
Expand All @@ -57,7 +62,11 @@ export function refreshOpencodeCliVersion(): Promise<string> {
return Promise.resolve(cachedVersion);
}
if (inFlight) return inFlight;
if (Date.now() < retryAfterFailureAt) {
return Promise.resolve(cachedVersion ?? OPENCODE_CLI_VERSION_FALLBACK);
}
inFlight = (async () => {
let answered = false;
try {
const response = await globalThis.fetch(NPM_OPENCODE_LATEST_URL, {
headers: { Accept: "application/json" },
Expand All @@ -68,10 +77,12 @@ export function refreshOpencodeCliVersion(): Promise<string> {
if (isOpencodeCliVersion(version)) {
cachedVersion = version;
cachedAt = Date.now();
answered = true;
}
} catch {
// Registry unreachable — stale cache / pin stay valid.
}
retryAfterFailureAt = answered ? 0 : Date.now() + BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS;
return cachedVersion ?? OPENCODE_CLI_VERSION_FALLBACK;
})();
const current = inFlight;
Expand All @@ -83,6 +94,7 @@ export function refreshOpencodeCliVersion(): Promise<string> {

/** Test seam: clear the cache. */
export function resetOpencodeCliVersionCache(): void {
retryAfterFailureAt = 0;
cachedVersion = null;
cachedAt = 0;
inFlight = null;
Expand Down
19 changes: 17 additions & 2 deletions src/shared/constants/claudeCodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
* uses that version when it is newer than the pin. The dashboard override
* (`cliVersions.ts`) wins over everything, then the env override. The lookup is fire-and-forget; a cold or failed fetch stays on the pin.
*/
import { canStartBackgroundRefresh } from "../utils/backgroundRefresh";
import {
BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS,
canStartBackgroundRefresh,
} from "../utils/backgroundRefresh";
import {
getCliVersionOverride,
type CliVersionSource,
Expand Down Expand Up @@ -40,6 +43,9 @@ type FetchLike = typeof fetch;
let cachedVersion: string | null = null;
let cachedAt = 0;
let inFlight: Promise<string> | null = null;
// Earliest time the getter may start another background lookup: one TTL after any answer
// (the cache below only keeps versions newer than the pin), the retry window after a failure.
let nextBackgroundRefreshAt = 0;

function getSafeEnvValue(name: string, pattern: RegExp): string | null {
const raw = typeof process === "undefined" ? undefined : process.env?.[name];
Expand Down Expand Up @@ -94,7 +100,12 @@ export function getClaudeCodeClientVersion(): string {
getCliVersionOverride("claude") ||
getSafeEnvValue(CLAUDE_VERSION_OVERRIDE_ENV, SAFE_HEADER_TOKEN_PATTERN);
if (override) return override;
if (shouldAutoRefreshClaudeCodeVersion() && !readFreshCache() && !inFlight) {
if (
shouldAutoRefreshClaudeCodeVersion() &&
!readFreshCache() &&
!inFlight &&
Date.now() >= nextBackgroundRefreshAt
) {
void refreshClaudeCodeClientVersion();
}
return pickAtLeastPin(readFreshCache());
Expand Down Expand Up @@ -156,6 +167,9 @@ export function refreshClaudeCodeClientVersion(fetchImpl: FetchLike = fetch): Pr
cachedVersion = resolved;
cachedAt = Date.now();
}
nextBackgroundRefreshAt =
Date.now() +
(resolved ? CLAUDE_CODE_VERSION_CACHE_TTL_MS : BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS);
return pickAtLeastPin(resolved ?? readFreshCache());
})();

Expand All @@ -168,6 +182,7 @@ export function refreshClaudeCodeClientVersion(fetchImpl: FetchLike = fetch): Pr

/** Test seam: drop the registry cache so the next refresh hits the network. */
export function resetClaudeCodeClientVersionCache(): void {
nextBackgroundRefreshAt = 0;
cachedVersion = null;
cachedAt = 0;
inFlight = null;
Expand Down
19 changes: 17 additions & 2 deletions src/shared/constants/geminiCliClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
* version when it is a dotted triple newer than the pin. The lookup is
* fire-and-forget; a cold or failed fetch stays on the pin.
*/
import { canStartBackgroundRefresh } from "../utils/backgroundRefresh";
import {
BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS,
canStartBackgroundRefresh,
} from "../utils/backgroundRefresh";

export const GEMINI_CLI_CLIENT_VERSION = "0.1.0";
export const GEMINI_CLI_USER_AGENT_SUFFIX = "(linux; x64)";
Expand All @@ -24,6 +27,9 @@ type FetchLike = typeof fetch;
let cachedVersion: string | null = null;
let cachedAt = 0;
let inFlight: Promise<string> | null = null;
// Earliest time the getter may start another background lookup: one TTL after any answer
// (the cache below only keeps versions newer than the pin), the retry window after a failure.
let nextBackgroundRefreshAt = 0;

function parseDottedTriple(value: unknown): string | null {
if (typeof value !== "string") return null;
Expand Down Expand Up @@ -64,7 +70,12 @@ function shouldAutoRefreshGeminiCliVersion(): boolean {
* itself never waits on the network.
*/
export function getGeminiCliClientVersion(): string {
if (shouldAutoRefreshGeminiCliVersion() && !readFreshCache() && !inFlight) {
if (
shouldAutoRefreshGeminiCliVersion() &&
!readFreshCache() &&
!inFlight &&
Date.now() >= nextBackgroundRefreshAt
) {
void refreshGeminiCliClientVersion();
}
return pickAtLeastPin(readFreshCache());
Expand Down Expand Up @@ -114,6 +125,9 @@ export function refreshGeminiCliClientVersion(fetchImpl: FetchLike = fetch): Pro
cachedVersion = resolved;
cachedAt = Date.now();
}
nextBackgroundRefreshAt =
Date.now() +
(resolved ? GEMINI_CLI_VERSION_CACHE_TTL_MS : BACKGROUND_REFRESH_RETRY_AFTER_FAILURE_MS);
return pickAtLeastPin(resolved ?? readFreshCache());
})();

Expand All @@ -126,6 +140,7 @@ export function refreshGeminiCliClientVersion(fetchImpl: FetchLike = fetch): Pro

/** Test seam: drop the registry cache so the next refresh hits the network. */
export function resetGeminiCliClientVersionCache(): void {
nextBackgroundRefreshAt = 0;
cachedVersion = null;
cachedAt = 0;
inFlight = null;
Expand Down
Loading
Loading