@dbx-tools/appkit-mastra 0.1.41 → 0.1.48

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.
@@ -1,28 +1,14 @@
1
1
  /**
2
- * Dynamic model resolution against Databricks Model Serving.
2
+ * Mastra-specific glue over the generic `@dbx-tools/model` toolkit.
3
3
  *
4
- * Turns loose, human-typed model names into real serving-endpoint ids
5
- * so the same agent can target different endpoints without code
6
- * changes. The workspace's `/serving-endpoints` list is fetched once
7
- * per host and cached with a TTL, with concurrent callers sharing one
8
- * in-flight promise (the coalescing pattern of Python's
9
- * `cachetools-async`). Names are matched through `fuse.js` extended
10
- * search so tokens like `"claude sonnet"` snap to
11
- * `databricks-claude-sonnet-4-6`, and a per-request override can be
12
- * pulled from the request header, query string, or body so a model
13
- * can be swapped per call without redeploying.
4
+ * The live `/serving-endpoints` catalogue access, fuzzy name
5
+ * resolution, and class/fallback selection all live in
6
+ * `@dbx-tools/model`; this module only adds what is specific to the
7
+ * Mastra plugin: pulling a per-request model override off an HTTP
8
+ * request (header / query / body) and projecting the plugin config
9
+ * onto the knobs `buildModel` and the `/models` route share.
14
10
  */
15
- import { type getExecutionContext } from "@databricks/appkit";
16
- import type { ServingEndpointSummary } from "@dbx-tools/appkit-mastra-shared";
17
11
  import type { MastraPluginConfig } from "./config.js";
18
- export type { ServingEndpointSummary };
19
- /**
20
- * Structural type for the Databricks workspace client. Derived from
21
- * AppKit's `ExecutionContext` so this module doesn't take a direct
22
- * dependency on `@databricks/sdk-experimental`; the dep flows in
23
- * transitively through `@databricks/appkit`.
24
- */
25
- type WorkspaceClientLike = ReturnType<typeof getExecutionContext>["client"];
26
12
  /**
27
13
  * `RequestContext` key under which {@link MastraServer} stores the
28
14
  * per-request model override (header / query / body). `model.ts`
@@ -35,76 +21,6 @@ export declare const MODEL_OVERRIDE_HEADER = "x-mastra-model";
35
21
  export declare const MODEL_OVERRIDE_QUERY = "model";
36
22
  /** Body fields (in priority order) inspected for a per-request model override. */
37
23
  export declare const MODEL_OVERRIDE_BODY_FIELDS: readonly ["model", "modelId"];
38
- /**
39
- * List Model Serving endpoints for the workspace owning `client`,
40
- * routed through AppKit's `CacheManager`. The manager gives us
41
- * everything `cachetools.TTLCache` provides plus what
42
- * `cachetools-async` adds on top: per-entry TTL, in-flight request
43
- * coalescing (concurrent callers share one fetch via the manager's
44
- * internal `inFlightRequests` map), bounded size, telemetry spans
45
- * (`cache.getOrExecute`), and optional Lakebase persistence so the
46
- * catalogue survives restarts when the lakebase plugin is wired up.
47
- *
48
- * Returns plain {@link ServingEndpointSummary} objects (a stable
49
- * subset of the SDK type) so cache hits never expose stale SDK
50
- * internals. Errors from `CacheManager` or the SDK fetch propagate
51
- * to the caller - we don't swallow them so users see the real
52
- * auth / network issue.
53
- *
54
- * @param host - Workspace host used as the cache key. Pass the value
55
- * resolved from `client.config.getHost()` so multi-host apps share
56
- * one entry per workspace.
57
- * @param opts.ttlMs - Override the default TTL just for this call.
58
- * Forwarded to `CacheManager` as seconds.
59
- */
60
- export declare function listServingEndpoints(client: WorkspaceClientLike, host: string, opts?: {
61
- ttlMs?: number;
62
- }): Promise<ServingEndpointSummary[]>;
63
- /**
64
- * Force-evict cached endpoint listings via AppKit's `CacheManager`.
65
- * With a `host` deletes that one workspace's entry; without one
66
- * clears every cache entry on the manager (since `CacheManager`
67
- * doesn't expose a namespace-scoped clear, this is the brute-force
68
- * path - fine for tests, avoid in steady-state code).
69
- */
70
- export declare function clearServingEndpointsCache(host?: string): Promise<void>;
71
- /**
72
- * Result of fuzzy-resolving a user-supplied model name against the
73
- * live endpoint list. `score` is Fuse.js's distance (`0` is exact,
74
- * `1` is no match); `matched` is `false` when the score exceeds the
75
- * configured threshold so callers can fall back to the original
76
- * input (Databricks will then return a clean 404).
77
- */
78
- export interface ResolvedModel {
79
- modelId: string;
80
- matched: boolean;
81
- score?: number;
82
- }
83
- /** Options accepted by {@link resolveModelId}. */
84
- export interface ResolveModelOptions {
85
- /** Fuse.js threshold (0 = exact, 1 = anything). Default `0.4`. */
86
- threshold?: number;
87
- }
88
- /**
89
- * Snap a user-supplied model name to the closest configured serving
90
- * endpoint:
91
- *
92
- * 1. Exact name match wins immediately (no fuzzy needed).
93
- * 2. Otherwise the input is tokenized (dashes / underscores / spaces
94
- * become separators) and fed through Fuse.js extended search,
95
- * which AND-s each token with fuzzy matching enabled. This is the
96
- * "tokenized fuzzy match" the user reaches for when they type
97
- * `"claude sonnet"` instead of the full endpoint name.
98
- * 3. If the best Fuse score is above `threshold`, return the input
99
- * unchanged and let the upstream call surface the 404. This keeps
100
- * deliberate model ids (e.g. brand new endpoints) from being
101
- * silently rewritten to a similar-looking neighbour.
102
- *
103
- * Pass an empty endpoint list to short-circuit fuzzy matching - the
104
- * input is returned verbatim. This is what {@link buildModel} does
105
- * when the workspace client can't be reached at resolve time.
106
- */
107
- export declare function resolveModelId(input: string, endpoints: readonly ServingEndpointSummary[], opts?: ResolveModelOptions): ResolvedModel;
108
24
  /**
109
25
  * Minimal Express-ish request shape used by {@link extractModelOverride}.
110
26
  * Keeps this module independent of `express` so the helper can be
@@ -132,15 +48,15 @@ export interface ModelOverrideRequest {
132
48
  export declare function extractModelOverride(req: ModelOverrideRequest): string | null;
133
49
  /**
134
50
  * Read the fuzzy-resolution config knobs off the plugin config with
135
- * defaults applied. Kept here so `buildModel` and the `/models` route
136
- * agree on what "enabled" means.
51
+ * `@dbx-tools/model` defaults applied. Kept here so `buildModel` and
52
+ * the `/models` route agree on what "enabled" means.
137
53
  *
138
- * `fallbacks` is the priority-ordered list `pickModelId` walks when
139
- * nothing explicit is set; defaults live in `model.ts`
140
- * (`FALLBACK_MODEL_IDS`) and are passed in by callers to avoid a
141
- * circular import between `serving.ts` and `model.ts`.
54
+ * `fallbacks` is the priority-ordered list `resolveModel` walks first
55
+ * when nothing explicit is set; defaults to an empty list so the
56
+ * generic resolver falls through to the live catalogue and its own
57
+ * `FALLBACK_MODEL_IDS` floor.
142
58
  */
143
- export declare function resolveServingConfig(config: MastraPluginConfig, defaultFallbacks?: readonly string[]): {
59
+ export declare function resolveServingConfig(config: MastraPluginConfig): {
144
60
  ttlMs: number;
145
61
  threshold: number;
146
62
  fuzzy: boolean;
@@ -1,21 +1,15 @@
1
1
  /**
2
- * Dynamic model resolution against Databricks Model Serving.
2
+ * Mastra-specific glue over the generic `@dbx-tools/model` toolkit.
3
3
  *
4
- * Turns loose, human-typed model names into real serving-endpoint ids
5
- * so the same agent can target different endpoints without code
6
- * changes. The workspace's `/serving-endpoints` list is fetched once
7
- * per host and cached with a TTL, with concurrent callers sharing one
8
- * in-flight promise (the coalescing pattern of Python's
9
- * `cachetools-async`). Names are matched through `fuse.js` extended
10
- * search so tokens like `"claude sonnet"` snap to
11
- * `databricks-claude-sonnet-4-6`, and a per-request override can be
12
- * pulled from the request header, query string, or body so a model
13
- * can be swapped per call without redeploying.
4
+ * The live `/serving-endpoints` catalogue access, fuzzy name
5
+ * resolution, and class/fallback selection all live in
6
+ * `@dbx-tools/model`; this module only adds what is specific to the
7
+ * Mastra plugin: pulling a per-request model override off an HTTP
8
+ * request (header / query / body) and projecting the plugin config
9
+ * onto the knobs `buildModel` and the `/models` route share.
14
10
  */
15
- import { CacheManager } from "@databricks/appkit";
16
- import { logUtils, stringUtils } from "@dbx-tools/shared";
17
- import Fuse from "fuse.js";
18
- const log = logUtils.logger("mastra/serving");
11
+ import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_MODEL_CACHE_TTL_MS } from "@dbx-tools/model";
12
+ import { stringUtils } from "@dbx-tools/shared";
19
13
  /**
20
14
  * `RequestContext` key under which {@link MastraServer} stores the
21
15
  * per-request model override (header / query / body). `model.ts`
@@ -28,145 +22,6 @@ export const MODEL_OVERRIDE_HEADER = "x-mastra-model";
28
22
  export const MODEL_OVERRIDE_QUERY = "model";
29
23
  /** Body fields (in priority order) inspected for a per-request model override. */
30
24
  export const MODEL_OVERRIDE_BODY_FIELDS = ["model", "modelId"];
31
- /** Default TTL for the in-memory endpoint cache. Matches the Databricks SDK's session lifetime budget. */
32
- const DEFAULT_TTL_MS = 5 * 60 * 1000;
33
- /** Default Fuse.js score threshold below which a fuzzy match is accepted. */
34
- const DEFAULT_FUZZY_THRESHOLD = 0.4;
35
- /** Cache key parts under which endpoint listings are stored. */
36
- const CACHE_KEY_NAMESPACE = "mastra:serving-endpoints";
37
- /**
38
- * Stable `userKey` arg for AppKit's `CacheManager.getOrExecute`.
39
- * Endpoint visibility is effectively workspace-scoped (we cache by
40
- * host in the key parts), so a single shared key lets every user of
41
- * the same workspace share one cached fetch and coalesce on the
42
- * in-flight promise. Permissions can differ in theory, but the
43
- * Foundation Model API catalogue is the same view for every caller.
44
- */
45
- const SHARED_USER_KEY = "mastra-shared";
46
- /**
47
- * List Model Serving endpoints for the workspace owning `client`,
48
- * routed through AppKit's `CacheManager`. The manager gives us
49
- * everything `cachetools.TTLCache` provides plus what
50
- * `cachetools-async` adds on top: per-entry TTL, in-flight request
51
- * coalescing (concurrent callers share one fetch via the manager's
52
- * internal `inFlightRequests` map), bounded size, telemetry spans
53
- * (`cache.getOrExecute`), and optional Lakebase persistence so the
54
- * catalogue survives restarts when the lakebase plugin is wired up.
55
- *
56
- * Returns plain {@link ServingEndpointSummary} objects (a stable
57
- * subset of the SDK type) so cache hits never expose stale SDK
58
- * internals. Errors from `CacheManager` or the SDK fetch propagate
59
- * to the caller - we don't swallow them so users see the real
60
- * auth / network issue.
61
- *
62
- * @param host - Workspace host used as the cache key. Pass the value
63
- * resolved from `client.config.getHost()` so multi-host apps share
64
- * one entry per workspace.
65
- * @param opts.ttlMs - Override the default TTL just for this call.
66
- * Forwarded to `CacheManager` as seconds.
67
- */
68
- export async function listServingEndpoints(client, host, opts = {}) {
69
- const ttlSec = Math.max(1, Math.round((opts.ttlMs ?? DEFAULT_TTL_MS) / 1000));
70
- return CacheManager.getInstanceSync().getOrExecute([CACHE_KEY_NAMESPACE, host], () => fetchEndpoints(client), SHARED_USER_KEY, { ttl: ttlSec });
71
- }
72
- async function fetchEndpoints(client) {
73
- const startedAt = Date.now();
74
- const out = [];
75
- for await (const ep of client.servingEndpoints.list()) {
76
- if (!ep.name)
77
- continue;
78
- out.push({
79
- name: ep.name,
80
- ...(ep.task !== undefined ? { task: ep.task } : {}),
81
- ...(ep.state?.ready !== undefined ? { state: String(ep.state.ready) } : {}),
82
- ...(ep.description !== undefined ? { description: ep.description } : {}),
83
- });
84
- }
85
- log.debug("listed", { count: out.length, elapsedMs: Date.now() - startedAt });
86
- return out;
87
- }
88
- /**
89
- * Force-evict cached endpoint listings via AppKit's `CacheManager`.
90
- * With a `host` deletes that one workspace's entry; without one
91
- * clears every cache entry on the manager (since `CacheManager`
92
- * doesn't expose a namespace-scoped clear, this is the brute-force
93
- * path - fine for tests, avoid in steady-state code).
94
- */
95
- export async function clearServingEndpointsCache(host) {
96
- const cache = CacheManager.getInstanceSync();
97
- if (host) {
98
- const key = cache.generateKey([CACHE_KEY_NAMESPACE, host], SHARED_USER_KEY);
99
- await cache.delete(key);
100
- }
101
- else {
102
- await cache.clear();
103
- }
104
- }
105
- /**
106
- * Snap a user-supplied model name to the closest configured serving
107
- * endpoint:
108
- *
109
- * 1. Exact name match wins immediately (no fuzzy needed).
110
- * 2. Otherwise the input is tokenized (dashes / underscores / spaces
111
- * become separators) and fed through Fuse.js extended search,
112
- * which AND-s each token with fuzzy matching enabled. This is the
113
- * "tokenized fuzzy match" the user reaches for when they type
114
- * `"claude sonnet"` instead of the full endpoint name.
115
- * 3. If the best Fuse score is above `threshold`, return the input
116
- * unchanged and let the upstream call surface the 404. This keeps
117
- * deliberate model ids (e.g. brand new endpoints) from being
118
- * silently rewritten to a similar-looking neighbour.
119
- *
120
- * Pass an empty endpoint list to short-circuit fuzzy matching - the
121
- * input is returned verbatim. This is what {@link buildModel} does
122
- * when the workspace client can't be reached at resolve time.
123
- */
124
- export function resolveModelId(input, endpoints, opts = {}) {
125
- if (endpoints.length === 0) {
126
- log.debug("resolve:no-endpoints", { input });
127
- return { modelId: input, matched: false };
128
- }
129
- for (const ep of endpoints) {
130
- if (ep.name === input) {
131
- log.debug("resolve:exact", { input });
132
- return { modelId: ep.name, matched: true, score: 0 };
133
- }
134
- }
135
- const threshold = opts.threshold ?? DEFAULT_FUZZY_THRESHOLD;
136
- const fuse = new Fuse(endpoints, {
137
- keys: ["name"],
138
- threshold,
139
- ignoreLocation: true,
140
- includeScore: true,
141
- useExtendedSearch: true,
142
- isCaseSensitive: false,
143
- });
144
- // Fuse 7.3 has no built-in tokenize hook; in extended search,
145
- // space-separated tokens are AND-ed with fuzzy matching enabled. We
146
- // lean on the shared tokenizer so the splitting rules stay
147
- // consistent with the rest of the toolkit.
148
- const query = Array.from(stringUtils.tokenizeWithOptions({ lowerCase: true, camelCase: false }, input)).join(" ");
149
- if (!query) {
150
- log.debug("resolve:empty-tokens", { input });
151
- return { modelId: input, matched: false };
152
- }
153
- const results = fuse.search(query);
154
- const best = results[0];
155
- if (best?.item.name && (best.score ?? 0) <= threshold) {
156
- log.debug("resolve:fuzzy-match", {
157
- input,
158
- modelId: best.item.name,
159
- score: best.score,
160
- });
161
- return { modelId: best.item.name, matched: true, score: best.score };
162
- }
163
- log.debug("resolve:no-match", {
164
- input,
165
- bestScore: best?.score,
166
- threshold,
167
- });
168
- return { modelId: input, matched: false };
169
- }
170
25
  /**
171
26
  * Pull a model override out of a single HTTP request, checking
172
27
  * sources in priority order:
@@ -205,20 +60,20 @@ export function extractModelOverride(req) {
205
60
  }
206
61
  /**
207
62
  * Read the fuzzy-resolution config knobs off the plugin config with
208
- * defaults applied. Kept here so `buildModel` and the `/models` route
209
- * agree on what "enabled" means.
63
+ * `@dbx-tools/model` defaults applied. Kept here so `buildModel` and
64
+ * the `/models` route agree on what "enabled" means.
210
65
  *
211
- * `fallbacks` is the priority-ordered list `pickModelId` walks when
212
- * nothing explicit is set; defaults live in `model.ts`
213
- * (`FALLBACK_MODEL_IDS`) and are passed in by callers to avoid a
214
- * circular import between `serving.ts` and `model.ts`.
66
+ * `fallbacks` is the priority-ordered list `resolveModel` walks first
67
+ * when nothing explicit is set; defaults to an empty list so the
68
+ * generic resolver falls through to the live catalogue and its own
69
+ * `FALLBACK_MODEL_IDS` floor.
215
70
  */
216
- export function resolveServingConfig(config, defaultFallbacks = []) {
71
+ export function resolveServingConfig(config) {
217
72
  return {
218
- ttlMs: config.modelCacheTtlMs ?? DEFAULT_TTL_MS,
73
+ ttlMs: config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS,
219
74
  threshold: config.modelFuzzyThreshold ?? DEFAULT_FUZZY_THRESHOLD,
220
75
  fuzzy: config.modelFuzzyMatch !== false,
221
76
  allowOverride: config.modelOverride !== false,
222
- fallbacks: config.defaultModelFallbacks ?? defaultFallbacks,
77
+ fallbacks: config.defaultModelFallbacks ?? [],
223
78
  };
224
79
  }
@@ -9,7 +9,7 @@
9
9
  * swallowed so a cancelled request or a client that navigated
10
10
  * away can't crash a tool mid-flight.
11
11
  */
12
- import type { MinimalWriter } from "@dbx-tools/appkit-mastra-shared";
12
+ import type { MastraWriter } from "@dbx-tools/appkit-mastra-shared";
13
13
  import { type logUtils } from "@dbx-tools/shared";
14
14
  /**
15
15
  * Best-effort `writer.write`. No-op when `writer` is undefined;
@@ -20,4 +20,4 @@ import { type logUtils } from "@dbx-tools/shared";
20
20
  *
21
21
  * Returns when the write resolves or rejects; never throws.
22
22
  */
23
- export declare function safeWrite(log: logUtils.Logger, writer: MinimalWriter | undefined, chunk: unknown, context?: Record<string, unknown>): Promise<void>;
23
+ export declare function safeWrite(log: logUtils.Logger, writer: MastraWriter | undefined, chunk: unknown, context?: Record<string, unknown>): Promise<void>;