@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/README.md +4 -1
- package/index.ts +2 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +3 -3
- package/lib/src/smart-router.d.ts +15 -16
- package/lib/src/smart-router.js +84 -137
- package/package.json +8 -7
- package/src/smart-router.ts +102 -147
package/package.json
CHANGED
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@databricks/appkit": "0.81.0",
|
|
30
|
-
"@dbx-tools/
|
|
31
|
-
"@dbx-tools/
|
|
32
|
-
"@dbx-tools/model
|
|
33
|
-
"@dbx-tools/
|
|
34
|
-
"@dbx-tools/shared-
|
|
35
|
-
"@dbx-tools/shared-model
|
|
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.
|
|
55
|
+
"version": "0.9.177",
|
|
55
56
|
"types": "./lib/index.d.ts",
|
|
56
57
|
"dbxToolsConfig": {
|
|
57
58
|
"tags": [
|
package/src/smart-router.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
191
|
-
private readonly
|
|
192
|
-
private
|
|
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
|
-
|
|
199
|
+
settings: Partial<SmartRouterSettings> = {},
|
|
196
200
|
private readonly invoke: SmartRouterInvoker = invokeSmartRouter,
|
|
197
201
|
private readonly probeStore: SmartRouterProbeStore = new AppKitSmartRouterProbeStore(),
|
|
198
202
|
) {
|
|
199
|
-
this.
|
|
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
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
248
|
+
/** Stop process-local probe caches after their active refreshes settle. */
|
|
242
249
|
dispose(): void {
|
|
243
|
-
|
|
244
|
-
this.
|
|
245
|
-
this.probeEntries.clear();
|
|
250
|
+
for (const cache of this.probeCaches) void closeCache(cache);
|
|
251
|
+
this.probeCaches.clear();
|
|
246
252
|
}
|
|
247
253
|
|
|
248
|
-
private
|
|
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
|
-
):
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
321
|
-
this.
|
|
322
|
-
void this.
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
338
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
341
|
+
key: string,
|
|
342
|
+
probe: SmartRouterProbeRecord,
|
|
360
343
|
client: WorkspaceClient,
|
|
361
344
|
): Promise<void> {
|
|
362
345
|
try {
|
|
363
346
|
await this.probeStore.set(
|
|
364
|
-
|
|
365
|
-
|
|
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):
|
|
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 =
|
|
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
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
404
|
-
|
|
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
|
-
|
|
516
|
-
service =
|
|
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.
|
|
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
|
-
});
|