@dbx-tools/appkit-model-gateway 0.9.176 → 0.9.177

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/package.json CHANGED
@@ -27,12 +27,13 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@databricks/appkit": "0.81.0",
30
- "@dbx-tools/databricks": "^0.9.176",
31
- "@dbx-tools/model": "^0.9.176",
32
- "@dbx-tools/model-protocol": "^0.9.176",
33
- "@dbx-tools/shared-core": "^0.9.176",
34
- "@dbx-tools/shared-model": "^0.9.176",
35
- "@dbx-tools/shared-model-gateway": "^0.9.176",
30
+ "@dbx-tools/cache": "^0.9.177",
31
+ "@dbx-tools/databricks": "^0.9.177",
32
+ "@dbx-tools/model": "^0.9.177",
33
+ "@dbx-tools/model-protocol": "^0.9.177",
34
+ "@dbx-tools/shared-core": "^0.9.177",
35
+ "@dbx-tools/shared-model": "^0.9.177",
36
+ "@dbx-tools/shared-model-gateway": "^0.9.177",
36
37
  "ai": "^7.0.0",
37
38
  "js-tiktoken": "^1.0.21",
38
39
  "zod": "4.3.6"
@@ -51,7 +52,7 @@
51
52
  "./package.json": "./package.json"
52
53
  }
53
54
  },
54
- "version": "0.9.176",
55
+ "version": "0.9.177",
55
56
  "types": "./lib/index.d.ts",
56
57
  "dbxToolsConfig": {
57
58
  "tags": [
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { createHash } from "node:crypto";
8
8
  import { CacheManager, getExecutionContext, type WorkspaceClient } from "@databricks/appkit";
9
+ import { closeCache, createCache, getOrSet } from "@dbx-tools/cache";
9
10
  import { refreshWorkspaceClientAuthentication } from "@dbx-tools/databricks";
10
11
  import { isRecord, log, toError } from "@dbx-tools/shared-core";
11
12
  import { ReasoningEffortSchema, type ReasoningEffort } from "@dbx-tools/shared-model";
@@ -46,6 +47,16 @@ export interface SmartRouterSettings {
46
47
  readonly timeoutMs: number;
47
48
  }
48
49
 
50
+ /** Defaults shared by gateway, Mastra, and CLI Smart Router callers. */
51
+ export const SMART_ROUTER_DEFAULT_SETTINGS: SmartRouterSettings = Object.freeze({
52
+ contextTokenBudget: 4_096,
53
+ probeRefreshIntervalMs: 3_600_000,
54
+ probeTtlMs: 300_000,
55
+ routeTtlSeconds: 21_600,
56
+ routerName: SMART_ROUTER_NAME,
57
+ timeoutMs: 10_000,
58
+ });
59
+
49
60
  /** One route option associated with the caller-owned value it represents. */
50
61
  export interface SmartRouterCandidate<T> {
51
62
  readonly harness: SmartRouterHarness;
@@ -158,15 +169,7 @@ export class AppKitSmartRouterProbeStore implements SmartRouterProbeStore {
158
169
  }
159
170
  }
160
171
 
161
- interface ProbeEntry {
162
- readonly cache: Map<string, ProbeEntry>;
163
- readonly candidates: readonly SmartRouterCandidate<unknown>[];
164
- readonly client: WeakRef<WorkspaceClient>;
165
- readonly key: string;
166
- expiresAt: number;
167
- refresh?: Promise<boolean>;
168
- value?: boolean;
169
- }
172
+ type SmartRouterProbeCache = ReturnType<typeof createCache>;
170
173
 
171
174
  /** Optional identity and cancellation inputs for one Smart Router call. */
172
175
  export interface SmartRouterSelectOptions {
@@ -187,16 +190,18 @@ type SmartRouterInvoker = (
187
190
  */
188
191
  export class SmartRouterService {
189
192
  readonly context: SmartRouterContextBuilder;
190
- private readonly probes = new WeakMap<object, Map<string, ProbeEntry>>();
191
- private readonly probeEntries = new Set<ProbeEntry>();
192
- private probeRefreshTimer?: NodeJS.Timeout;
193
+ private readonly settings: SmartRouterSettings;
194
+ private readonly probes = new WeakMap<object, SmartRouterProbeCache>();
195
+ private readonly probeKeys = new WeakMap<object, Set<string>>();
196
+ private readonly probeCaches = new Set<SmartRouterProbeCache>();
193
197
 
194
198
  constructor(
195
- private readonly settings: SmartRouterSettings,
199
+ settings: Partial<SmartRouterSettings> = {},
196
200
  private readonly invoke: SmartRouterInvoker = invokeSmartRouter,
197
201
  private readonly probeStore: SmartRouterProbeStore = new AppKitSmartRouterProbeStore(),
198
202
  ) {
199
- this.context = new SmartRouterContextBuilder(settings.contextTokenBudget);
203
+ this.settings = { ...SMART_ROUTER_DEFAULT_SETTINGS, ...settings };
204
+ this.context = new SmartRouterContextBuilder(this.settings.contextTokenBudget);
200
205
  }
201
206
 
202
207
  /** Select one offered route for arbitrary input content. */
@@ -227,85 +232,49 @@ export class SmartRouterService {
227
232
  if (offered.length === 0) return false;
228
233
  const cache = this.probeCache(client);
229
234
  const key = probeKey(offered, this.settings.routerName);
230
- const now = Date.now();
231
- const cached = cache.get(key);
232
- if (cached) {
233
- if (cached.value === undefined) return cached.refresh!;
234
- if (cached.expiresAt <= now && !cached.refresh) void this.refreshProbe(cached);
235
- return cached.value;
236
- }
237
- const entry = this.createProbeEntry(cache, key, offered, client);
238
- return this.initializeProbe(entry);
235
+ this.rememberProbeKey(client, key);
236
+ const probe = await getOrSet(
237
+ cache,
238
+ key,
239
+ () => this.loadProbe(key, offered, client),
240
+ {
241
+ ttl: this.probeCacheTtlMs(),
242
+ refreshAfterWrite: (value) => this.probeRefreshAfterWrite(value.enabled),
243
+ },
244
+ );
245
+ return probe?.enabled ?? false;
239
246
  }
240
247
 
241
- /** Stop this service's unref'd background probe refresh timer. */
248
+ /** Stop process-local probe caches after their active refreshes settle. */
242
249
  dispose(): void {
243
- if (this.probeRefreshTimer) clearInterval(this.probeRefreshTimer);
244
- this.probeRefreshTimer = undefined;
245
- this.probeEntries.clear();
250
+ for (const cache of this.probeCaches) void closeCache(cache);
251
+ this.probeCaches.clear();
246
252
  }
247
253
 
248
- private createProbeEntry<T>(
249
- cache: Map<string, ProbeEntry>,
254
+ private async loadProbe<T>(
250
255
  key: string,
251
256
  candidates: readonly SmartRouterCandidate<T>[],
252
257
  client: WorkspaceClient,
253
- ): ProbeEntry {
254
- const entry: ProbeEntry = {
255
- cache,
256
- candidates,
257
- client: new WeakRef(client),
258
- key,
259
- expiresAt: 0,
260
- };
261
- this.replaceProbe(cache, key, entry);
262
- return entry;
263
- }
264
-
265
- private initializeProbe(entry: ProbeEntry): Promise<boolean> {
266
- const client = entry.client.deref();
267
- if (!client) return Promise.resolve(false);
268
- let refresh!: Promise<boolean>;
269
- refresh = this.readStoredProbe(entry, client)
270
- .then((stored) => {
271
- if (stored && stored.expiresAt > Date.now()) {
272
- entry.value = stored.enabled;
273
- entry.expiresAt = stored.expiresAt;
274
- return stored.enabled;
275
- }
276
- return this.executeProbe(entry, client);
277
- })
278
- .finally(() => {
279
- if (entry.refresh === refresh) entry.refresh = undefined;
280
- });
281
- entry.refresh = refresh;
282
- return refresh;
258
+ ): Promise<SmartRouterProbeRecord> {
259
+ const stored = await this.readStoredProbe(key, client);
260
+ if (stored && stored.expiresAt > Date.now()) return stored;
261
+ return this.executeProbe(key, candidates, client);
283
262
  }
284
263
 
285
- private refreshProbe(entry: ProbeEntry): Promise<boolean> {
286
- if (entry.refresh) return entry.refresh;
287
- const client = entry.client.deref();
288
- if (!client) return Promise.resolve(false);
289
- let refresh!: Promise<boolean>;
290
- refresh = this.executeProbe(entry, client).finally(() => {
291
- if (entry.refresh === refresh) entry.refresh = undefined;
292
- });
293
- entry.refresh = refresh;
294
- return refresh;
295
- }
296
-
297
- private async executeProbe(entry: ProbeEntry, client: WorkspaceClient): Promise<boolean> {
298
- const enabled = await this.selectTask(PROBE_TASK, entry.candidates, { client })
264
+ private async executeProbe<T>(
265
+ key: string,
266
+ candidates: readonly SmartRouterCandidate<T>[],
267
+ client: WorkspaceClient,
268
+ ): Promise<SmartRouterProbeRecord> {
269
+ const enabled = await this.selectTask(PROBE_TASK, candidates, { client })
299
270
  .then(() => true)
300
271
  .catch((error) => {
301
272
  logger.debug("Smart Router capability probe unavailable", { error: toError(error) });
302
273
  return false;
303
274
  });
304
- if (entry.cache.get(entry.key) === entry) {
305
- this.updateProbeEntry(entry, enabled);
306
- void this.writeStoredProbe(entry, enabled, client);
307
- }
308
- return enabled;
275
+ const probe = this.probeRecord(enabled);
276
+ void this.writeStoredProbe(key, probe, client);
277
+ return probe;
309
278
  }
310
279
 
311
280
  private setProbeValue<T>(
@@ -317,9 +286,10 @@ export class SmartRouterService {
317
286
  if (offered.length === 0) return;
318
287
  const cache = this.probeCache(client);
319
288
  const key = probeKey(offered, this.settings.routerName);
320
- const entry = cache.get(key) ?? this.createProbeEntry(cache, key, offered, client);
321
- this.updateProbeEntry(entry, enabled);
322
- void this.writeStoredProbe(entry, enabled, client);
289
+ this.rememberProbeKey(client, key);
290
+ const probe = this.probeRecord(enabled);
291
+ void this.writeProcessProbe(cache, key, probe);
292
+ void this.writeStoredProbe(key, probe, client);
323
293
  }
324
294
 
325
295
  private setProbeUnavailable<T>(
@@ -327,26 +297,27 @@ export class SmartRouterService {
327
297
  client: WorkspaceClient,
328
298
  ): void {
329
299
  const cache = this.probeCache(client);
330
- for (const cached of cache.values()) {
331
- this.updateProbeEntry(cached, false);
332
- void this.writeStoredProbe(cached, false, client);
300
+ const offered = uniqueCandidates(candidates);
301
+ const requestedKey = probeKey(offered, this.settings.routerName);
302
+ this.rememberProbeKey(client, requestedKey);
303
+ const keys = this.probeKeys.get(client.config as object) ?? new Set([requestedKey]);
304
+ for (const key of keys) {
305
+ const probe = this.probeRecord(false);
306
+ void this.writeProcessProbe(cache, key, probe);
307
+ void this.writeStoredProbe(key, probe, client);
333
308
  }
334
- this.setProbeValue(candidates, client, false);
335
309
  }
336
310
 
337
- private updateProbeEntry(entry: ProbeEntry, enabled: boolean): void {
338
- entry.value = enabled;
339
- entry.expiresAt = Date.now() + this.probeTtl(enabled);
311
+ private probeRecord(enabled: boolean): SmartRouterProbeRecord {
312
+ return { enabled, expiresAt: Date.now() + this.probeTtl(enabled) };
340
313
  }
341
314
 
342
315
  private async readStoredProbe(
343
- entry: ProbeEntry,
316
+ key: string,
344
317
  client: WorkspaceClient,
345
318
  ): Promise<SmartRouterProbeRecord | undefined> {
346
319
  try {
347
- const parsed = SmartRouterProbeRecordSchema.safeParse(
348
- await this.probeStore.get(entry.key, client),
349
- );
320
+ const parsed = SmartRouterProbeRecordSchema.safeParse(await this.probeStore.get(key, client));
350
321
  return parsed.success ? parsed.data : undefined;
351
322
  } catch (error) {
352
323
  logger.debug("Smart Router probe cache read failed", { error: toError(error) });
@@ -354,21 +325,28 @@ export class SmartRouterService {
354
325
  }
355
326
  }
356
327
 
328
+ private async writeProcessProbe(
329
+ cache: SmartRouterProbeCache,
330
+ key: string,
331
+ probe: SmartRouterProbeRecord,
332
+ ): Promise<void> {
333
+ try {
334
+ await cache.set(key, probe, { ttl: this.probeCacheTtlMs() });
335
+ } catch (error) {
336
+ logger.debug("Smart Router process probe cache write failed", { error: toError(error) });
337
+ }
338
+ }
339
+
357
340
  private async writeStoredProbe(
358
- entry: ProbeEntry,
359
- enabled: boolean,
341
+ key: string,
342
+ probe: SmartRouterProbeRecord,
360
343
  client: WorkspaceClient,
361
344
  ): Promise<void> {
362
345
  try {
363
346
  await this.probeStore.set(
364
- entry.key,
365
- { enabled, expiresAt: entry.expiresAt },
366
- Math.max(
367
- 1,
368
- Math.ceil(
369
- (Math.max(this.settings.probeRefreshIntervalMs, this.settings.probeTtlMs) * 2) / 1_000,
370
- ),
371
- ),
347
+ key,
348
+ probe,
349
+ Math.max(1, Math.ceil(this.probeCacheTtlMs() / 1_000)),
372
350
  client,
373
351
  );
374
352
  } catch (error) {
@@ -376,22 +354,29 @@ export class SmartRouterService {
376
354
  }
377
355
  }
378
356
 
379
- private probeCache(client: WorkspaceClient): Map<string, ProbeEntry> {
357
+ private probeCache(client: WorkspaceClient): SmartRouterProbeCache {
380
358
  const owner = client.config as object;
381
359
  let cache = this.probes.get(owner);
382
360
  if (!cache) {
383
- cache = new Map();
361
+ cache = createCache({ maxEntries: 32, useClone: false });
384
362
  this.probes.set(owner, cache);
363
+ this.probeCaches.add(cache);
385
364
  }
386
365
  return cache;
387
366
  }
388
367
 
389
- private replaceProbe(cache: Map<string, ProbeEntry>, key: string, entry: ProbeEntry): void {
390
- const previous = cache.get(key);
391
- if (previous) this.probeEntries.delete(previous);
392
- cache.set(key, entry);
393
- this.probeEntries.add(entry);
394
- this.ensureProbeRefreshTimer();
368
+ private rememberProbeKey(client: WorkspaceClient, key: string): void {
369
+ const owner = client.config as object;
370
+ let keys = this.probeKeys.get(owner);
371
+ if (!keys) {
372
+ keys = new Set();
373
+ this.probeKeys.set(owner, keys);
374
+ }
375
+ keys.add(key);
376
+ }
377
+
378
+ private probeCacheTtlMs(): number {
379
+ return Math.max(this.settings.probeRefreshIntervalMs, this.settings.probeTtlMs) * 2;
395
380
  }
396
381
 
397
382
  private probeTtl(enabled: boolean): number {
@@ -400,30 +385,8 @@ export class SmartRouterService {
400
385
  : Math.min(this.settings.probeTtlMs, NEGATIVE_PROBE_TTL_MS);
401
386
  }
402
387
 
403
- private ensureProbeRefreshTimer(): void {
404
- if (this.probeRefreshTimer) return;
405
- this.probeRefreshTimer = setInterval(() => {
406
- void this.refreshProbes();
407
- }, this.settings.probeRefreshIntervalMs);
408
- this.probeRefreshTimer.unref();
409
- }
410
-
411
- private async refreshProbes(): Promise<void> {
412
- const refreshes: Promise<boolean>[] = [];
413
- for (const entry of [...this.probeEntries]) {
414
- if (entry.cache.get(entry.key) !== entry) {
415
- this.probeEntries.delete(entry);
416
- continue;
417
- }
418
- const client = entry.client.deref();
419
- if (!client) {
420
- entry.cache.delete(entry.key);
421
- this.probeEntries.delete(entry);
422
- continue;
423
- }
424
- if (!entry.refresh) refreshes.push(this.refreshProbe(entry));
425
- }
426
- await Promise.allSettled(refreshes);
388
+ private probeRefreshAfterWrite(enabled: boolean): number {
389
+ return Math.min(this.settings.probeRefreshIntervalMs, this.probeTtl(enabled));
427
390
  }
428
391
 
429
392
  private async selectTask<T>(
@@ -510,14 +473,16 @@ export class SmartRouterContextBuilder {
510
473
  /** Smart Router orchestration shared by Responses, Chat Completions, and Messages. */
511
474
  export class SmartModelRouter {
512
475
  readonly service: SmartRouterService;
476
+ private readonly settings: SmartRouterSettings;
513
477
 
514
478
  constructor(
515
- private readonly settings: SmartRouterSettings,
516
- service = new SmartRouterService(settings),
479
+ settings: Partial<SmartRouterSettings> = {},
480
+ service: SmartRouterService | undefined = undefined,
517
481
  private readonly store: SmartRouterStore = new AppKitSmartRouterStore(),
518
482
  private readonly client?: WorkspaceClient,
519
483
  ) {
520
- this.service = service;
484
+ this.settings = { ...SMART_ROUTER_DEFAULT_SETTINGS, ...settings };
485
+ this.service = service ?? new SmartRouterService(this.settings);
521
486
  }
522
487
 
523
488
  /** Probe whether Smart Router is usable for the current model catalogue. */
@@ -971,13 +936,3 @@ async function invokeSmartRouter(
971
936
  }
972
937
  return response.json();
973
938
  }
974
-
975
- /** Defaults shared by gateway, Mastra, and CLI Smart Router callers. */
976
- export const SMART_ROUTER_DEFAULT_SETTINGS: SmartRouterSettings = Object.freeze({
977
- contextTokenBudget: 4_096,
978
- probeRefreshIntervalMs: 3_600_000,
979
- probeTtlMs: 300_000,
980
- routeTtlSeconds: 21_600,
981
- routerName: SMART_ROUTER_NAME,
982
- timeoutMs: 10_000,
983
- });