jeopi-catalog 16.5.6 → 16.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.6.0] - 2026-10-01
6
+
7
+ ### Fixed
8
+
9
+ - Refresh Antigravity's live model catalog with lazily resolved OAuth access, account-and-endpoint-scoped caching, and bounded cancellation. Successful discovery replaces stale bundled entries, including an empty catalog; failed refreshes retain the last authoritative snapshot without extending its lifetime.
10
+ - Preserve authoritative empty Antigravity results during provider-only catalog regeneration while retaining previous entries on discovery failure.
11
+
5
12
  ## [16.5.6] - 2026-10-01
6
13
 
7
14
  ### Fixed
@@ -28,6 +28,8 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
28
28
  cacheTtlMs?: number;
29
29
  /** When true, a successful dynamic fetch is the complete provider catalog and prunes static-only models. */
30
30
  dynamicModelsAuthoritative?: boolean;
31
+ /** Keep a prior complete catalog (including empty) on failed/offline refresh, without extending its TTL. */
32
+ preserveAuthoritativeCacheOnFailure?: boolean;
31
33
  /** Cached model ids to ignore when the cache was written against a different static catalog fingerprint. */
32
34
  dropCachedModelIdsOnStaticMismatch?: readonly string[];
33
35
  /** Trusted provider-wide headers restored locally, never persisted. */
@@ -59,6 +61,8 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
59
61
  export interface ModelResolutionResult<TApi extends Api = Api> {
60
62
  models: Model<TApi>[];
61
63
  stale: boolean;
64
+ /** A stale result retained a complete provider snapshot; callers must not resurrect bundled-only ids. */
65
+ preservedAuthoritativeCache?: boolean;
62
66
  }
63
67
  /**
64
68
  * Stateful facade over provider model resolution.
@@ -15,6 +15,10 @@ export interface GoogleAntigravityModelManagerConfig {
15
15
  oauthToken?: string;
16
16
  endpoint?: string;
17
17
  fetch?: FetchImpl;
18
+ /** Stable credential identity; falls back to the token when unavailable. */
19
+ cacheScope?: string;
20
+ /** Resolve a fresh token only when discovery actually needs the network. */
21
+ resolveOAuthToken?: (signal: AbortSignal) => Promise<string | undefined>;
18
22
  }
19
23
  export interface GoogleGeminiCliModelManagerConfig {
20
24
  oauthToken?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-catalog",
4
- "version": "16.5.6",
4
+ "version": "16.6.1",
5
5
  "description": "Model catalog for jeopi: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://github.com/akillness/jeopi",
7
7
  "author": "Can Boluk",
@@ -34,12 +34,12 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@bufbuild/protobuf": "^2.12.0",
37
- "jeopi-utils": "16.5.6",
37
+ "jeopi-utils": "16.6.1",
38
38
  "arktype": "2.2.3",
39
39
  "zod": "^4"
40
40
  },
41
41
  "devDependencies": {
42
- "jeopi-ai": "16.5.6",
42
+ "jeopi-ai": "16.6.1",
43
43
  "@types/bun": "^1.3.14"
44
44
  },
45
45
  "engines": {
@@ -161,12 +161,16 @@ export interface FetchAntigravityDiscoveryModelsOptions {
161
161
  export async function fetchAntigravityDiscoveryModels(
162
162
  options: FetchAntigravityDiscoveryModelsOptions,
163
163
  ): Promise<ModelSpec<"google-gemini-cli">[] | null> {
164
+ if (!options.token.trim() || options.signal?.aborted) return null;
165
+ const timeout = AbortSignal.timeout(10_000);
166
+ const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
164
167
  const fetcher = discoveryFetch(options.fetcher);
165
168
  const endpoints = options.endpoint
166
169
  ? [trimTrailingSlashes(options.endpoint)]
167
170
  : DEFAULT_ANTIGRAVITY_DISCOVERY_ENDPOINTS.map(trimTrailingSlashes);
168
171
 
169
172
  for (const endpoint of endpoints) {
173
+ if (signal.aborted) return null;
170
174
  let response: Response;
171
175
  try {
172
176
  response = await fetcher(`${endpoint}${FETCH_AVAILABLE_MODELS_PATH}`, {
@@ -177,7 +181,7 @@ export async function fetchAntigravityDiscoveryModels(
177
181
  "User-Agent": options.userAgent ?? getAntigravityUserAgent(),
178
182
  },
179
183
  body: JSON.stringify({}),
180
- signal: options.signal,
184
+ signal,
181
185
  });
182
186
  } catch {
183
187
  continue;
@@ -194,6 +198,7 @@ export async function fetchAntigravityDiscoveryModels(
194
198
  continue;
195
199
  }
196
200
 
201
+ if (signal.aborted) return null;
197
202
  const parsed = parseAntigravityDiscoveryResponse(payload);
198
203
  if (!parsed) {
199
204
  continue;
@@ -241,6 +246,13 @@ export async function fetchAntigravityDiscoveryModels(
241
246
  }
242
247
 
243
248
  function parseAntigravityDiscoveryResponse(value: unknown): AntigravityDiscoveryApiResponse | null {
249
+ // An absent or corrupt map is a failed discovery, never an authoritative empty catalog.
250
+ if (typeof value !== "object" || value === null || !("models" in value)) return null;
251
+ const models = value.models;
252
+ if (typeof models !== "object" || models === null || Array.isArray(models)) return null;
253
+ for (const [id, model] of Object.entries(models)) {
254
+ if (!id.trim() || typeof model !== "object" || model === null || Array.isArray(model)) return null;
255
+ }
244
256
  const parsed = AntigravityDiscoveryApiResponseSchema(value);
245
257
  if (parsed instanceof type.errors) {
246
258
  return null;
@@ -39,6 +39,8 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
39
39
  cacheTtlMs?: number;
40
40
  /** When true, a successful dynamic fetch is the complete provider catalog and prunes static-only models. */
41
41
  dynamicModelsAuthoritative?: boolean;
42
+ /** Keep a prior complete catalog (including empty) on failed/offline refresh, without extending its TTL. */
43
+ preserveAuthoritativeCacheOnFailure?: boolean;
42
44
  /** Cached model ids to ignore when the cache was written against a different static catalog fingerprint. */
43
45
  dropCachedModelIdsOnStaticMismatch?: readonly string[];
44
46
  /** Trusted provider-wide headers restored locally, never persisted. */
@@ -71,6 +73,8 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
71
73
  export interface ModelResolutionResult<TApi extends Api = Api> {
72
74
  models: Model<TApi>[];
73
75
  stale: boolean;
76
+ /** A stale result retained a complete provider snapshot; callers must not resurrect bundled-only ids. */
77
+ preservedAuthoritativeCache?: boolean;
74
78
  }
75
79
 
76
80
  /**
@@ -209,6 +213,20 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
209
213
  const shouldUseFreshCacheAsAuthoritative =
210
214
  strategy === "online-if-uncached" && hasUsableFreshCache && hasAuthoritativeCache;
211
215
  const dynamicFetchSucceeded = fetchedDynamicModels !== null;
216
+ if (
217
+ !dynamicFetchSucceeded &&
218
+ dynamicModelsAuthoritative &&
219
+ options.preserveAuthoritativeCacheOnFailure &&
220
+ cache?.authoritative &&
221
+ cacheFingerprintMatches &&
222
+ !cacheHasUnresolvedHeaders
223
+ ) {
224
+ return {
225
+ models: collapseBuiltModelVariants(restoredCache.models),
226
+ stale: true,
227
+ preservedAuthoritativeCache: true,
228
+ };
229
+ }
212
230
  const cacheModels = dynamicFetchSucceeded
213
231
  ? []
214
232
  : prepareCacheModelsForStaticMismatch(
@@ -1,4 +1,4 @@
1
- import { fetchAntigravityDiscoveryModels } from "../discovery/antigravity";
1
+ import { ANTIGRAVITY_PRIMARY_ENDPOINT, fetchAntigravityDiscoveryModels } from "../discovery/antigravity";
2
2
  import { fetchGeminiModels } from "../discovery/gemini";
3
3
  import type { ModelManagerOptions } from "../model-manager";
4
4
  import type { FetchImpl } from "../types";
@@ -21,6 +21,10 @@ export interface GoogleAntigravityModelManagerConfig {
21
21
  oauthToken?: string;
22
22
  endpoint?: string;
23
23
  fetch?: FetchImpl;
24
+ /** Stable credential identity; falls back to the token when unavailable. */
25
+ cacheScope?: string;
26
+ /** Resolve a fresh token only when discovery actually needs the network. */
27
+ resolveOAuthToken?: (signal: AbortSignal) => Promise<string | undefined>;
24
28
  }
25
29
 
26
30
  export interface GoogleGeminiCliModelManagerConfig {
@@ -60,17 +64,26 @@ export function googleVertexModelManagerOptions(_config?: GoogleVertexModelManag
60
64
  export function googleAntigravityModelManagerOptions(
61
65
  config?: GoogleAntigravityModelManagerConfig,
62
66
  ): ModelManagerOptions<"google-gemini-cli"> {
63
- const token = config?.oauthToken;
67
+ const token = config?.oauthToken?.trim();
68
+ const endpoint = (config?.endpoint ?? ANTIGRAVITY_PRIMARY_ENDPOINT).replace(/\/+$/, "");
64
69
  return {
65
70
  providerId: "google-antigravity",
66
71
  ...(token
67
72
  ? {
68
- fetchDynamicModels: () =>
69
- fetchAntigravityDiscoveryModels({
70
- token,
73
+ cacheProviderId: `google-antigravity:discovery-v2:${Bun.hash(JSON.stringify([endpoint, config?.cacheScope ?? token])).toString(36)}`,
74
+ dynamicModelsAuthoritative: true,
75
+ preserveAuthoritativeCacheOnFailure: true,
76
+ fetchDynamicModels: async () => {
77
+ const signal = AbortSignal.timeout(10_000);
78
+ const resolvedToken = config?.resolveOAuthToken ? await config.resolveOAuthToken(signal) : token;
79
+ if (!resolvedToken?.trim() || signal.aborted) return null;
80
+ return fetchAntigravityDiscoveryModels({
81
+ token: resolvedToken,
71
82
  endpoint: config?.endpoint,
72
83
  fetcher: toDiscoveryFetch(config?.fetch),
73
- }),
84
+ signal,
85
+ });
86
+ },
74
87
  }
75
88
  : undefined),
76
89
  };