@dbx-tools/model 0.9.118 → 0.9.119
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/README.md +10 -8
- package/index.ts +2 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +2 -2
- package/lib/src/_cache.d.ts +4 -0
- package/lib/src/_cache.js +12 -0
- package/lib/src/metadata-cache.d.ts +10 -52
- package/lib/src/metadata-cache.js +32 -186
- package/lib/src/metadata.d.ts +3 -5
- package/lib/src/metadata.js +7 -10
- package/lib/src/model-catalog.d.ts +9 -19
- package/lib/src/model-catalog.js +24 -26
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -9
- package/src/_cache.ts +13 -0
- package/src/metadata-cache.ts +51 -245
- package/src/metadata.ts +6 -10
- package/src/model-catalog.ts +29 -31
- package/lib/package.json +0 -137
package/src/metadata-cache.ts
CHANGED
|
@@ -1,72 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Uses cacache's built-in memory memoization (`memoize: true` on put; gets read
|
|
5
|
-
* the in-memory layer by default) with content-addressed disk under
|
|
6
|
-
* `env-paths("dbx-tools").cache/model-gateway/<version>/`. Disk persistence is
|
|
7
|
-
* disabled inside a Databricks App ({@link environmentUtils.isDatabricksAppEnv});
|
|
8
|
-
* those processes keep a process-local Map only.
|
|
9
|
-
*
|
|
10
|
-
* Each cache owns a key, hard-coded fallback, optional network loader, and a
|
|
11
|
-
* merger that combines fresh / previous / fallback values. Freshness is the
|
|
12
|
-
* envelope `refreshedAt` window (default one day), not cacache's short LRU TTL.
|
|
2
|
+
* Daily model metadata cache built on the shared memory-first cache package.
|
|
13
3
|
*
|
|
14
4
|
* @module
|
|
15
5
|
*/
|
|
16
6
|
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
import * as environmentUtils from "@dbx-tools/shared-core/environment-utils";
|
|
7
|
+
import { createCache, getOrSet } from "@dbx-tools/cache/cache";
|
|
20
8
|
import * as log from "@dbx-tools/shared-core/log";
|
|
21
|
-
import cacache from "cacache";
|
|
22
|
-
import envPaths from "env-paths";
|
|
23
9
|
|
|
24
|
-
import
|
|
10
|
+
import { modelMetadataCache } from "./_cache.ts";
|
|
25
11
|
import { MODEL_METADATA_TTL_MS } from "./_metadata-contract.ts";
|
|
26
12
|
|
|
27
13
|
const logger = log.logger("model/metadata-cache");
|
|
28
14
|
|
|
29
|
-
interface PackageIdentity {
|
|
30
|
-
readonly identifier: string;
|
|
31
|
-
readonly version: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const PACKAGE_IDENTITY: PackageIdentity = {
|
|
35
|
-
identifier: packageManifest.name,
|
|
36
|
-
version: packageManifest.version,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/** Envelope stored as JSON bytes in cacache for every key. */
|
|
40
|
-
export interface MetadataCacheRecord<T> {
|
|
41
|
-
readonly refreshedAt: number;
|
|
42
|
-
readonly value: T;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
15
|
/** Factory inputs for one named metadata cache. */
|
|
46
16
|
export interface MetadataCacheOptions<T> {
|
|
47
|
-
/** Stable
|
|
17
|
+
/** Stable cache key. */
|
|
48
18
|
readonly key: string;
|
|
49
19
|
/** Hard-coded / committed baseline used when store and load both miss. */
|
|
50
20
|
readonly fallback: T;
|
|
51
|
-
/**
|
|
52
|
-
* Refresh source. When omitted the cache only serves store + fallback and
|
|
53
|
-
* {@link MetadataCache.update} mutations (for example error-learned values).
|
|
54
|
-
*/
|
|
21
|
+
/** Optional refresh source. */
|
|
55
22
|
readonly load?: () => Promise<T>;
|
|
56
23
|
/** Combine a fresh load, prior store value, and the hard-coded fallback. */
|
|
57
24
|
readonly merge: (input: MetadataCacheMergeInput<T>) => T;
|
|
58
|
-
/** Freshness window; defaults to
|
|
25
|
+
/** Freshness window; defaults to one day. */
|
|
59
26
|
readonly ttlMs?: number;
|
|
60
|
-
/** Package version segment in the cache path. Defaults to this package. */
|
|
61
|
-
readonly version?: string;
|
|
62
|
-
/** Override the cacache root directory (tests). */
|
|
63
|
-
readonly cacheDir?: string;
|
|
64
|
-
/**
|
|
65
|
-
* Force disk on/off. Defaults to off inside Databricks Apps, on otherwise.
|
|
66
|
-
*/
|
|
67
|
-
readonly disk?: boolean;
|
|
68
|
-
/** Clock override for tests. */
|
|
69
|
-
readonly now?: () => number;
|
|
70
27
|
}
|
|
71
28
|
|
|
72
29
|
/** Arguments passed to a cache merger. */
|
|
@@ -76,15 +33,11 @@ export interface MetadataCacheMergeInput<T> {
|
|
|
76
33
|
readonly fallback: T;
|
|
77
34
|
}
|
|
78
35
|
|
|
79
|
-
/** One
|
|
36
|
+
/** One namespaced view over the shared metadata cache. */
|
|
80
37
|
export interface MetadataCache<T> {
|
|
81
|
-
/** Absolute cacache root for this package version (unused when disk is off). */
|
|
82
|
-
readonly path: string;
|
|
83
|
-
/** Whether cacache disk persistence is enabled for this instance. */
|
|
84
|
-
readonly diskEnabled: boolean;
|
|
85
38
|
/** Return a fresh-or-fallback value, refreshing when stale. */
|
|
86
39
|
get(): Promise<T>;
|
|
87
|
-
/** Read memory
|
|
40
|
+
/** Read process memory without refreshing. */
|
|
88
41
|
peek(): Promise<T | undefined>;
|
|
89
42
|
/** Mutate the cached value and persist it. */
|
|
90
43
|
update(mutator: (current: T) => T): Promise<T>;
|
|
@@ -92,161 +45,65 @@ export interface MetadataCache<T> {
|
|
|
92
45
|
refresh(): Promise<T>;
|
|
93
46
|
}
|
|
94
47
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return resolve(cacheDirectory, "model-gateway", version);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Whether metadata caches should persist to disk in this process. */
|
|
104
|
-
export function metadataCacheDiskEnabled(environment: NodeJS.ProcessEnv = process.env): boolean {
|
|
105
|
-
return !environmentUtils.isDatabricksAppEnv(environment);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Create one daily cache backed by cacache memoization, with optional disk.
|
|
110
|
-
*/
|
|
111
|
-
export function createMetadataCache<T>(options: MetadataCacheOptions<T>): MetadataCache<T> {
|
|
48
|
+
/** Create one namespaced view over the shared model metadata cache. */
|
|
49
|
+
export function createMetadataCache<T>(
|
|
50
|
+
options: MetadataCacheOptions<T>,
|
|
51
|
+
cache: ReturnType<typeof createCache> = modelMetadataCache,
|
|
52
|
+
): MetadataCache<T> {
|
|
112
53
|
const ttlMs = options.ttlMs ?? MODEL_METADATA_TTL_MS;
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const pathPromise = resolveCacheDir(options);
|
|
116
|
-
let path = options.cacheDir;
|
|
117
|
-
|
|
118
|
-
const flightKey = (cachePath: string, suffix = ""): string =>
|
|
119
|
-
`${cachePath}::${options.key}${suffix}`;
|
|
120
|
-
|
|
121
|
-
/** Process-local store used when disk is off (Databricks Apps). */
|
|
122
|
-
const memoryOnly = new Map<string, MetadataCacheRecord<T>>();
|
|
123
|
-
|
|
124
|
-
const resolveValue = (input: MetadataCacheMergeInput<T>): T => options.merge(input);
|
|
125
|
-
|
|
126
|
-
const isFresh = (record: MetadataCacheRecord<T> | undefined): boolean =>
|
|
127
|
-
Boolean(record && now() - record.refreshedAt < ttlMs);
|
|
128
|
-
|
|
129
|
-
const cachePath = async (): Promise<string> => {
|
|
130
|
-
path ??= await pathPromise;
|
|
131
|
-
return path;
|
|
132
|
-
};
|
|
54
|
+
const valueKey = `model-metadata:value:${options.key}`;
|
|
55
|
+
const refreshKey = `model-metadata:refresh:${options.key}`;
|
|
133
56
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
// cacache reads its in-memory memo first unless memoize is false
|
|
139
|
-
const entry = await cacache.get(resolvedPath, options.key);
|
|
140
|
-
return decodeRecord<T>(entry.data);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
if (isCacheMiss(error)) return undefined;
|
|
143
|
-
logger.warn("metadata cache cacache read failed", {
|
|
144
|
-
key: options.key,
|
|
145
|
-
path: resolvedPath,
|
|
146
|
-
error,
|
|
147
|
-
});
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const writeRecord = async (record: MetadataCacheRecord<T>): Promise<void> => {
|
|
153
|
-
if (!diskEnabled) {
|
|
154
|
-
memoryOnly.set(options.key, record);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
const resolvedPath = await cachePath();
|
|
158
|
-
try {
|
|
159
|
-
await cacache.put(resolvedPath, options.key, `${JSON.stringify(record)}\n`, {
|
|
160
|
-
memoize: true,
|
|
161
|
-
metadata: { refreshedAt: record.refreshedAt },
|
|
162
|
-
});
|
|
163
|
-
} catch (error) {
|
|
164
|
-
logger.warn("metadata cache cacache write failed", {
|
|
165
|
-
key: options.key,
|
|
166
|
-
path: resolvedPath,
|
|
167
|
-
error,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const persist = async (value: T, refreshedAt = now()): Promise<T> => {
|
|
173
|
-
await writeRecord({ refreshedAt, value });
|
|
57
|
+
const read = (): Promise<T | undefined> => cache.get<T>(valueKey);
|
|
58
|
+
const persist = async (value: T): Promise<T> => {
|
|
59
|
+
await cache.set(valueKey, value);
|
|
174
60
|
return value;
|
|
175
61
|
};
|
|
62
|
+
const resolveValue = (fresh: T | undefined, previous: T | undefined): T =>
|
|
63
|
+
options.merge({ fresh, previous, fallback: options.fallback });
|
|
176
64
|
|
|
177
65
|
const refresh = async (force: boolean): Promise<T> => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
error,
|
|
197
|
-
path: resolvedPath,
|
|
198
|
-
diskEnabled,
|
|
199
|
-
});
|
|
66
|
+
if (force) await cache.delete(refreshKey);
|
|
67
|
+
let staleRefreshFailed = false;
|
|
68
|
+
await getOrSet(
|
|
69
|
+
cache,
|
|
70
|
+
refreshKey,
|
|
71
|
+
async () => {
|
|
72
|
+
const previous = await read();
|
|
73
|
+
let fresh: T | undefined;
|
|
74
|
+
if (options.load) {
|
|
75
|
+
try {
|
|
76
|
+
fresh = await options.load();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.warn("metadata cache refresh failed; using cached or committed value", {
|
|
79
|
+
key: options.key,
|
|
80
|
+
error,
|
|
81
|
+
});
|
|
82
|
+
staleRefreshFailed = previous !== undefined;
|
|
83
|
+
}
|
|
200
84
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
await persist(value);
|
|
210
|
-
}
|
|
211
|
-
return value;
|
|
212
|
-
});
|
|
85
|
+
const resolved = resolveValue(fresh, previous);
|
|
86
|
+
if (fresh !== undefined || previous === undefined) await persist(resolved);
|
|
87
|
+
return true;
|
|
88
|
+
},
|
|
89
|
+
{ ttl: ttlMs, throwErrors: true },
|
|
90
|
+
);
|
|
91
|
+
if (staleRefreshFailed) await cache.delete(refreshKey);
|
|
92
|
+
return resolveValue(undefined, await read());
|
|
213
93
|
};
|
|
214
94
|
|
|
215
95
|
return {
|
|
216
|
-
get path() {
|
|
217
|
-
if (!path) {
|
|
218
|
-
throw new Error("metadata cache path is not resolved until the first cache read");
|
|
219
|
-
}
|
|
220
|
-
return path;
|
|
221
|
-
},
|
|
222
|
-
diskEnabled,
|
|
223
96
|
get: () => refresh(false),
|
|
224
|
-
peek:
|
|
97
|
+
peek: read,
|
|
225
98
|
refresh: () => refresh(true),
|
|
226
99
|
update: async (mutator) => {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const baseline = resolveValue({
|
|
231
|
-
fresh: undefined,
|
|
232
|
-
previous: previous?.value,
|
|
233
|
-
fallback: options.fallback,
|
|
234
|
-
});
|
|
235
|
-
return persist(mutator(baseline));
|
|
236
|
-
});
|
|
100
|
+
const value = await persist(mutator(resolveValue(undefined, await read())));
|
|
101
|
+
await cache.set(refreshKey, true, { ttl: ttlMs });
|
|
102
|
+
return value;
|
|
237
103
|
},
|
|
238
104
|
};
|
|
239
105
|
}
|
|
240
106
|
|
|
241
|
-
async function resolveCacheDir(options: {
|
|
242
|
-
readonly cacheDir?: string;
|
|
243
|
-
readonly version?: string;
|
|
244
|
-
}): Promise<string> {
|
|
245
|
-
if (options.cacheDir) return options.cacheDir;
|
|
246
|
-
const version = options.version ?? PACKAGE_IDENTITY.version;
|
|
247
|
-
return defaultMetadataCacheDir(version);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
107
|
/** Prefer a non-empty fresh array, else previous, else fallback. */
|
|
251
108
|
export function mergePreferFreshList<T>(input: MetadataCacheMergeInput<readonly T[]>): T[] {
|
|
252
109
|
if (input.fresh && input.fresh.length > 0) return [...input.fresh];
|
|
@@ -254,10 +111,7 @@ export function mergePreferFreshList<T>(input: MetadataCacheMergeInput<readonly
|
|
|
254
111
|
return [...input.fallback];
|
|
255
112
|
}
|
|
256
113
|
|
|
257
|
-
/**
|
|
258
|
-
* Shallow-merge record catalogues: fallback, then previous, then fresh keys.
|
|
259
|
-
* Fresh values win per key.
|
|
260
|
-
*/
|
|
114
|
+
/** Shallow-merge record catalogues: fallback, then previous, then fresh keys. */
|
|
261
115
|
export function mergePreferFreshRecord<T>(
|
|
262
116
|
input: MetadataCacheMergeInput<Readonly<Record<string, T>>>,
|
|
263
117
|
): Record<string, T> {
|
|
@@ -267,51 +121,3 @@ export function mergePreferFreshRecord<T>(
|
|
|
267
121
|
...(input.fresh ?? {}),
|
|
268
122
|
};
|
|
269
123
|
}
|
|
270
|
-
|
|
271
|
-
/** Test helper: wipe a cacache root and clear cacache memoization. */
|
|
272
|
-
export async function resetMetadataCacheDir(cacheDir: string): Promise<void> {
|
|
273
|
-
try {
|
|
274
|
-
await cacache.rm.all(cacheDir);
|
|
275
|
-
} catch {
|
|
276
|
-
// empty / missing cache is fine
|
|
277
|
-
}
|
|
278
|
-
cacache.clearMemoized();
|
|
279
|
-
for (const key of [...inflight.keys()]) {
|
|
280
|
-
if (key.startsWith(`${cacheDir}::`)) inflight.delete(key);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function coalesce<T>(key: string, work: () => Promise<T>): Promise<T> {
|
|
285
|
-
const existing = inflight.get(key);
|
|
286
|
-
if (existing) return existing as Promise<T>;
|
|
287
|
-
const pending = work().finally(() => {
|
|
288
|
-
if (inflight.get(key) === pending) inflight.delete(key);
|
|
289
|
-
});
|
|
290
|
-
inflight.set(key, pending);
|
|
291
|
-
return pending;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function decodeRecord<T>(data: Buffer): MetadataCacheRecord<T> | undefined {
|
|
295
|
-
try {
|
|
296
|
-
const parsed = JSON.parse(data.toString("utf8")) as unknown;
|
|
297
|
-
if (
|
|
298
|
-
!parsed ||
|
|
299
|
-
typeof parsed !== "object" ||
|
|
300
|
-
typeof (parsed as MetadataCacheRecord<T>).refreshedAt !== "number"
|
|
301
|
-
) {
|
|
302
|
-
return undefined;
|
|
303
|
-
}
|
|
304
|
-
return parsed as MetadataCacheRecord<T>;
|
|
305
|
-
} catch {
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function isCacheMiss(error: unknown): boolean {
|
|
311
|
-
return Boolean(
|
|
312
|
-
error &&
|
|
313
|
-
typeof error === "object" &&
|
|
314
|
-
"code" in error &&
|
|
315
|
-
(error as { code?: string }).code === "ENOENT",
|
|
316
|
-
);
|
|
317
|
-
}
|
package/src/metadata.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* docs and error-learned reasoning ladders.
|
|
5
5
|
*
|
|
6
6
|
* Committed snapshots seed every process. {@link refreshModelMetadata} pulls
|
|
7
|
-
* documentation through {@link createMetadataCache}
|
|
7
|
+
* documentation through the shared memory-first {@link createMetadataCache} and
|
|
8
8
|
* rebuilds the in-memory index. Error-learned efforts use the same store via
|
|
9
|
-
* {@link rememberReasoningLevels}.
|
|
9
|
+
* {@link rememberReasoningLevels}. Known-good values are written behind to the
|
|
10
|
+
* shared model project cache.
|
|
10
11
|
*
|
|
11
12
|
* @module
|
|
12
13
|
*/
|
|
@@ -251,7 +252,7 @@ export async function refreshModelMetadata(): Promise<MetadataIndex> {
|
|
|
251
252
|
return refreshInflight;
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
/** Load
|
|
255
|
+
/** Load shared cache entries without forcing a network refresh. */
|
|
255
256
|
export async function hydrateModelMetadata(): Promise<MetadataIndex> {
|
|
256
257
|
const [retiredNames, capabilities, rateLimits, reasoning, learnedReasoning] = await Promise.all([
|
|
257
258
|
peekOrFallback(retiredCache, COMMITTED_RETIRED_MODELS),
|
|
@@ -276,11 +277,6 @@ async function peekOrFallback<T>(cache: MetadataCache<T>, fallback: T): Promise<
|
|
|
276
277
|
return cache.get().catch(() => fallback);
|
|
277
278
|
}
|
|
278
279
|
|
|
279
|
-
/** Absolute cacache root used by the documentation / learned caches. */
|
|
280
|
-
export function modelMetadataCachePath(): string {
|
|
281
|
-
return retiredCache.path;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
280
|
export { modelStatusFor, retiredModelNames } from "./_retirement.ts";
|
|
285
281
|
|
|
286
282
|
/** Return the committed reasoning-effort catalogue. Cached per process. */
|
|
@@ -288,7 +284,7 @@ export function reasoningModelCatalogue(): ReasoningModelCatalogue {
|
|
|
288
284
|
return metadataIndex().reasoning;
|
|
289
285
|
}
|
|
290
286
|
|
|
291
|
-
/** Return error-learned reasoning ladders
|
|
287
|
+
/** Return error-learned reasoning ladders from the shared model cache. */
|
|
292
288
|
export function learnedReasoningCatalogue(): ReasoningModelCatalogue {
|
|
293
289
|
return metadataIndex().learnedReasoning;
|
|
294
290
|
}
|
|
@@ -324,7 +320,7 @@ export async function rememberReasoningLevels(
|
|
|
324
320
|
*
|
|
325
321
|
* Accepts the same nested Databricks envelopes as {@link parseReasoningLevels}.
|
|
326
322
|
* Returns the levels that were stored (or the current lookup when parse yields
|
|
327
|
-
* nothing). The raw error body is never
|
|
323
|
+
* nothing). The raw error body is never cached.
|
|
328
324
|
*/
|
|
329
325
|
export async function learnReasoningLevelsFromError(
|
|
330
326
|
model: string,
|
package/src/model-catalog.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Lists the workspace's `/serving-endpoints` and, when the caller supplies a
|
|
5
5
|
* trusted credential identity, caches the result per host and identity with a
|
|
6
|
-
* TTL via
|
|
6
|
+
* TTL via `@dbx-tools/cache`. Concurrent callers in the same scope share
|
|
7
7
|
* one in-flight promise. Surfaces each endpoint as a stable
|
|
8
8
|
* {@link ServingEndpointSummary} - including the Foundation Model API
|
|
9
9
|
* `quality` / `speed` / `cost` profile when present, the classified
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { createHash } from "node:crypto";
|
|
25
|
-
import { CacheManager } from "@databricks/appkit";
|
|
26
25
|
import { appkit } from "@dbx-tools/appkit";
|
|
26
|
+
import { deleteCachedValue, getOrSet } from "@dbx-tools/cache/cache";
|
|
27
27
|
import { errorUtils, log } from "@dbx-tools/shared-core";
|
|
28
28
|
import {
|
|
29
29
|
model,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type ServingEndpointSummary,
|
|
34
34
|
} from "@dbx-tools/shared-model";
|
|
35
35
|
|
|
36
|
+
import { modelCatalogueCache } from "./_cache.ts";
|
|
36
37
|
import { classifyEndpointClasses, normalizeEndpoints, rankEndpoints } from "./_ranking.ts";
|
|
37
38
|
|
|
38
39
|
const { ModelClass } = model;
|
|
@@ -54,12 +55,12 @@ export const DEFAULT_FUZZY_THRESHOLD = 0.4;
|
|
|
54
55
|
|
|
55
56
|
/** Cache key parts under which endpoint listings are stored. */
|
|
56
57
|
const CACHE_KEY_NAMESPACE = "serving-endpoints";
|
|
58
|
+
const catalogueCache = modelCatalogueCache;
|
|
57
59
|
|
|
58
60
|
/** Options for {@link listServingEndpoints}. */
|
|
59
61
|
export interface ListServingEndpointsOptions {
|
|
60
62
|
/**
|
|
61
|
-
* Override the default cache TTL for this call, in milliseconds.
|
|
62
|
-
* `CacheManager` as seconds.
|
|
63
|
+
* Override the default cache TTL for this call, in milliseconds.
|
|
63
64
|
*/
|
|
64
65
|
ttlMs?: number;
|
|
65
66
|
/**
|
|
@@ -73,30 +74,23 @@ export interface ListServingEndpointsOptions {
|
|
|
73
74
|
cacheIdentity?: string;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
/** Hash an opaque credential identity before handing it to
|
|
77
|
+
/** Hash an opaque credential identity before handing it to the shared cache. */
|
|
77
78
|
function cacheUserKey(identity: string): string {
|
|
78
79
|
return `model-${createHash("sha256").update(identity).digest("hex")}`;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
83
|
* List Model Serving endpoints for the workspace owning `client`, routed
|
|
83
|
-
* through
|
|
84
|
-
*
|
|
85
|
-
* per-entry TTL, in-flight request coalescing (concurrent callers share one
|
|
86
|
-
* fetch via the manager's internal `inFlightRequests` map), bounded size,
|
|
87
|
-
* telemetry spans (`cache.getOrExecute`), and optional Lakebase persistence so
|
|
88
|
-
* the catalogue survives restarts when the lakebase plugin is wired up.
|
|
84
|
+
* through the shared memory-first cache, including per-entry TTL, in-flight
|
|
85
|
+
* request coalescing, bounded process memory, and portable persistence.
|
|
89
86
|
*
|
|
90
87
|
* Returns plain {@link ServingEndpointSummary} objects (a stable subset of the
|
|
91
|
-
* SDK type) so cache hits never expose stale SDK internals.
|
|
92
|
-
* `CacheManager` or the SDK fetch propagate to the caller - we don't swallow
|
|
93
|
-
* them so users see the real auth / network issue.
|
|
88
|
+
* SDK type) so cache hits never expose stale SDK internals.
|
|
94
89
|
*
|
|
95
90
|
* @param host - Workspace host used as the cache key. Pass the value resolved
|
|
96
91
|
* from `client.config.getHost()` so multi-host apps share one entry per
|
|
97
92
|
* workspace.
|
|
98
|
-
* @param options.ttlMs - Override the default TTL just for this call.
|
|
99
|
-
* to `CacheManager` as seconds.
|
|
93
|
+
* @param options.ttlMs - Override the default TTL just for this call.
|
|
100
94
|
*/
|
|
101
95
|
export async function listServingEndpoints(
|
|
102
96
|
client: WorkspaceClientLike,
|
|
@@ -104,13 +98,17 @@ export async function listServingEndpoints(
|
|
|
104
98
|
options: ListServingEndpointsOptions = {},
|
|
105
99
|
): Promise<ServingEndpointSummary[]> {
|
|
106
100
|
if (!options.cacheIdentity) return fetchEndpoints(client);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
return (await getOrSet(
|
|
102
|
+
catalogueCache,
|
|
103
|
+
catalogueCacheKey(host, options.cacheIdentity),
|
|
110
104
|
() => fetchEndpoints(client),
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
{
|
|
106
|
+
ttl: options.ttlMs ?? DEFAULT_MODEL_CACHE_TTL_MS,
|
|
107
|
+
returnCachedOnError: true,
|
|
108
|
+
cachedOnErrorTtl: "7d",
|
|
109
|
+
throwErrors: true,
|
|
110
|
+
},
|
|
111
|
+
))!;
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
/**
|
|
@@ -119,7 +117,7 @@ export async function listServingEndpoints(
|
|
|
119
117
|
* none of the cache-load enrichment ({@link listServingEndpoints} adds the
|
|
120
118
|
* {@link ModelClass} stamp and the embedding-dimension probe). Use this for a
|
|
121
119
|
* one-shot, dependency-light listing - e.g. a CLI that only needs names/tasks
|
|
122
|
-
* for fuzzy resolution and doesn't want
|
|
120
|
+
* for fuzzy resolution and doesn't want shared cache persistence or the
|
|
123
121
|
* per-embedding ping cost. Prefer {@link listServingEndpoints} for the cached,
|
|
124
122
|
* enriched view.
|
|
125
123
|
*/
|
|
@@ -197,24 +195,24 @@ async function pingEmbeddingDimension(
|
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
/**
|
|
200
|
-
* Force-evict cached endpoint listings
|
|
201
|
-
*
|
|
202
|
-
* cannot enumerate a namespace, omitting either value clears the complete
|
|
203
|
-
* manager and should be reserved for tests or administrative refreshes.
|
|
198
|
+
* Force-evict cached endpoint listings. A host and identity delete only that
|
|
199
|
+
* principal's workspace entry. Omitting either value clears this catalogue.
|
|
204
200
|
*/
|
|
205
201
|
export async function clearServingEndpointsCache(
|
|
206
202
|
host?: string,
|
|
207
203
|
cacheIdentity?: string,
|
|
208
204
|
): Promise<void> {
|
|
209
|
-
const cache = CacheManager.getInstanceSync();
|
|
210
205
|
if (host && cacheIdentity) {
|
|
211
|
-
|
|
212
|
-
await cache.delete(key);
|
|
206
|
+
await deleteCachedValue(catalogueCache, catalogueCacheKey(host, cacheIdentity));
|
|
213
207
|
} else {
|
|
214
|
-
await
|
|
208
|
+
await catalogueCache.clear();
|
|
215
209
|
}
|
|
216
210
|
}
|
|
217
211
|
|
|
212
|
+
function catalogueCacheKey(host: string, cacheIdentity: string): string {
|
|
213
|
+
return JSON.stringify([CACHE_KEY_NAMESPACE, host, cacheUserKey(cacheIdentity)]);
|
|
214
|
+
}
|
|
215
|
+
|
|
218
216
|
/**
|
|
219
217
|
* Result of fuzzy-resolving a user-supplied model name against the live
|
|
220
218
|
* endpoint list. `score` is the ranker's distance (`0` is exact, `1` is no match);
|