@dbx-tools/model 0.1.42
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/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/src/fallback.d.ts +35 -0
- package/dist/src/fallback.js +65 -0
- package/dist/src/resolve.d.ts +88 -0
- package/dist/src/resolve.js +117 -0
- package/dist/src/serving.d.ts +96 -0
- package/dist/src/serving.js +188 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/index.ts +22 -0
- package/package.json +38 -0
- package/src/fallback.ts +72 -0
- package/src/resolve.ts +194 -0
- package/src/serving.ts +253 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dbx-tools/model` public surface.
|
|
3
|
+
*
|
|
4
|
+
* Bundles the package's Node-side Model Serving access (cached
|
|
5
|
+
* `/serving-endpoints` listing plus fuzzy name resolution), the
|
|
6
|
+
* workspace-aware {@link resolveModel} selector, and the server-only
|
|
7
|
+
* offline fallback opinion (`FALLBACK_MODEL_IDS` / `modelsForTier`)
|
|
8
|
+
* with a re-export of the pure `@dbx-tools/model-shared` surface
|
|
9
|
+
* (capability tiers, the score profile, the endpoint descriptor, and
|
|
10
|
+
* the tier classifier) so a single `from "@dbx-tools/model"` import
|
|
11
|
+
* serves server-side consumers.
|
|
12
|
+
*
|
|
13
|
+
* Browser-side consumers should import `@dbx-tools/model-shared`
|
|
14
|
+
* directly: the serving driver pulls in `WorkspaceClient` and AppKit's
|
|
15
|
+
* `CacheManager` and is Node-only, whereas the re-exported surface is
|
|
16
|
+
* pure (types + sync functions) and safe for any runtime.
|
|
17
|
+
*/
|
|
18
|
+
export * from "@dbx-tools/model-shared";
|
|
19
|
+
export * from "./src/fallback.js";
|
|
20
|
+
export * from "./src/resolve.js";
|
|
21
|
+
export * from "./src/serving.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dbx-tools/model` public surface.
|
|
3
|
+
*
|
|
4
|
+
* Bundles the package's Node-side Model Serving access (cached
|
|
5
|
+
* `/serving-endpoints` listing plus fuzzy name resolution), the
|
|
6
|
+
* workspace-aware {@link resolveModel} selector, and the server-only
|
|
7
|
+
* offline fallback opinion (`FALLBACK_MODEL_IDS` / `modelsForTier`)
|
|
8
|
+
* with a re-export of the pure `@dbx-tools/model-shared` surface
|
|
9
|
+
* (capability tiers, the score profile, the endpoint descriptor, and
|
|
10
|
+
* the tier classifier) so a single `from "@dbx-tools/model"` import
|
|
11
|
+
* serves server-side consumers.
|
|
12
|
+
*
|
|
13
|
+
* Browser-side consumers should import `@dbx-tools/model-shared`
|
|
14
|
+
* directly: the serving driver pulls in `WorkspaceClient` and AppKit's
|
|
15
|
+
* `CacheManager` and is Node-only, whereas the re-exported surface is
|
|
16
|
+
* pure (types + sync functions) and safe for any runtime.
|
|
17
|
+
*/
|
|
18
|
+
export * from "@dbx-tools/model-shared";
|
|
19
|
+
export * from "./src/fallback.js";
|
|
20
|
+
export * from "./src/resolve.js";
|
|
21
|
+
export * from "./src/serving.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side offline fallback opinion for model selection.
|
|
3
|
+
*
|
|
4
|
+
* When the live `/serving-endpoints` catalogue can't be read at all -
|
|
5
|
+
* no user token, the service principal can't list, or the workspace is
|
|
6
|
+
* unreachable - the resolver still has to name *some* endpoint. This
|
|
7
|
+
* module holds that floor: a small, hard-coded set of well-known
|
|
8
|
+
* Foundation Model API endpoint names, each bucketed into a
|
|
9
|
+
* {@link ModelTier} by the shared {@link classifyByFamily} heuristic
|
|
10
|
+
* (no tiers are hard-coded here) and ordered best-first.
|
|
11
|
+
*
|
|
12
|
+
* This is deliberately *server-only*. A browser client never talks to
|
|
13
|
+
* Databricks directly - it always goes through this server - so it has
|
|
14
|
+
* nothing to fall back to and must not assume a stale, baked-in model
|
|
15
|
+
* list; it consumes the live `/models` response instead. The pure
|
|
16
|
+
* classifier ({@link classifyEndpoints}) is what the client shares.
|
|
17
|
+
*/
|
|
18
|
+
import { ModelTier } from "@dbx-tools/model-shared";
|
|
19
|
+
/**
|
|
20
|
+
* Static fallback model ids for a tier, drawn from the small built-in
|
|
21
|
+
* {@link FALLBACK_MODEL_NAMES} list and ordered best-first by family
|
|
22
|
+
* rank. Sync and workspace-independent: this is the *fallback opinion*
|
|
23
|
+
* used to seed default lists or when the live catalogue is unreachable
|
|
24
|
+
* - live resolution prefers `classifyEndpoints`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function modelsForTier(tier: ModelTier): readonly string[];
|
|
27
|
+
/** Top static fallback model id for a tier. */
|
|
28
|
+
export declare function modelForTier(tier: ModelTier): string;
|
|
29
|
+
/**
|
|
30
|
+
* Priority-ordered fallback chain (Thinking -> Balanced -> Fast) over
|
|
31
|
+
* the small built-in list. The floor walked at resolve time when no
|
|
32
|
+
* agent / plugin / env / request model is set *and* the live
|
|
33
|
+
* catalogue yields nothing.
|
|
34
|
+
*/
|
|
35
|
+
export declare const FALLBACK_MODEL_IDS: readonly string[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side offline fallback opinion for model selection.
|
|
3
|
+
*
|
|
4
|
+
* When the live `/serving-endpoints` catalogue can't be read at all -
|
|
5
|
+
* no user token, the service principal can't list, or the workspace is
|
|
6
|
+
* unreachable - the resolver still has to name *some* endpoint. This
|
|
7
|
+
* module holds that floor: a small, hard-coded set of well-known
|
|
8
|
+
* Foundation Model API endpoint names, each bucketed into a
|
|
9
|
+
* {@link ModelTier} by the shared {@link classifyByFamily} heuristic
|
|
10
|
+
* (no tiers are hard-coded here) and ordered best-first.
|
|
11
|
+
*
|
|
12
|
+
* This is deliberately *server-only*. A browser client never talks to
|
|
13
|
+
* Databricks directly - it always goes through this server - so it has
|
|
14
|
+
* nothing to fall back to and must not assume a stale, baked-in model
|
|
15
|
+
* list; it consumes the live `/models` response instead. The pure
|
|
16
|
+
* classifier ({@link classifyEndpoints}) is what the client shares.
|
|
17
|
+
*/
|
|
18
|
+
import { classifyByFamily, ModelTier } from "@dbx-tools/model-shared";
|
|
19
|
+
/**
|
|
20
|
+
* Small, last-resort set of well-known Foundation Model API endpoint
|
|
21
|
+
* names, ordered best-first within each tier by {@link classifyByFamily}.
|
|
22
|
+
* Used only as the floor when the live `/serving-endpoints` catalogue
|
|
23
|
+
* can't be read at resolve time; the live, score-driven classification
|
|
24
|
+
* supersedes it whenever the workspace listing is available. Tiers are
|
|
25
|
+
* not hard-coded here - each name is classified by family heuristic.
|
|
26
|
+
*/
|
|
27
|
+
const FALLBACK_MODEL_NAMES = [
|
|
28
|
+
"databricks-claude-opus-4-8",
|
|
29
|
+
"databricks-gpt-5-5-pro",
|
|
30
|
+
"databricks-gemini-3-1-pro",
|
|
31
|
+
"databricks-claude-sonnet-4-6",
|
|
32
|
+
"databricks-gpt-5-5",
|
|
33
|
+
"databricks-meta-llama-3-3-70b-instruct",
|
|
34
|
+
"databricks-claude-haiku-4-5",
|
|
35
|
+
"databricks-gpt-5-nano",
|
|
36
|
+
"databricks-meta-llama-3-1-8b-instruct",
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Static fallback model ids for a tier, drawn from the small built-in
|
|
40
|
+
* {@link FALLBACK_MODEL_NAMES} list and ordered best-first by family
|
|
41
|
+
* rank. Sync and workspace-independent: this is the *fallback opinion*
|
|
42
|
+
* used to seed default lists or when the live catalogue is unreachable
|
|
43
|
+
* - live resolution prefers `classifyEndpoints`.
|
|
44
|
+
*/
|
|
45
|
+
export function modelsForTier(tier) {
|
|
46
|
+
return FALLBACK_MODEL_NAMES.map((name) => ({ name, c: classifyByFamily(name) }))
|
|
47
|
+
.filter((x) => x.c !== null && x.c.tier === tier)
|
|
48
|
+
.sort((a, b) => b.c.rank - a.c.rank)
|
|
49
|
+
.map((x) => x.name);
|
|
50
|
+
}
|
|
51
|
+
/** Top static fallback model id for a tier. */
|
|
52
|
+
export function modelForTier(tier) {
|
|
53
|
+
return modelsForTier(tier)[0];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Priority-ordered fallback chain (Thinking -> Balanced -> Fast) over
|
|
57
|
+
* the small built-in list. The floor walked at resolve time when no
|
|
58
|
+
* agent / plugin / env / request model is set *and* the live
|
|
59
|
+
* catalogue yields nothing.
|
|
60
|
+
*/
|
|
61
|
+
export const FALLBACK_MODEL_IDS = [
|
|
62
|
+
...modelsForTier(ModelTier.Thinking),
|
|
63
|
+
...modelsForTier(ModelTier.Balanced),
|
|
64
|
+
...modelsForTier(ModelTier.Fast),
|
|
65
|
+
];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-aware model selection.
|
|
3
|
+
*
|
|
4
|
+
* Given a caller's intent (an explicit name, a capability
|
|
5
|
+
* {@link ModelTier}, or nothing), the resolver returns the endpoint id
|
|
6
|
+
* to use - always one the workspace actually has, degrading from "best
|
|
7
|
+
* in tier" down to the static fallback floor. Two entry points:
|
|
8
|
+
*
|
|
9
|
+
* - {@link selectModel} is the high-level, I/O-doing call: hand it a
|
|
10
|
+
* `WorkspaceClient` and intent and it lists `/serving-endpoints`
|
|
11
|
+
* (cached) and resolves in one step. This is what a non-Mastra
|
|
12
|
+
* consumer (e.g. a job that just needs a model name) reaches for.
|
|
13
|
+
* - {@link resolveModel} is the pure form over an endpoint list you
|
|
14
|
+
* already hold; `selectModel` is a thin wrapper over it.
|
|
15
|
+
*/
|
|
16
|
+
import { ModelTier, type ServingEndpointSummary } from "@dbx-tools/model-shared";
|
|
17
|
+
import { type WorkspaceClientLike } from "./serving.js";
|
|
18
|
+
/** Caller intent passed to {@link resolveModel}. */
|
|
19
|
+
export interface ResolveModelInput {
|
|
20
|
+
/**
|
|
21
|
+
* Explicit model id / loose name (per-request override, agent /
|
|
22
|
+
* plugin default, or env var). When set it wins over `tier` and
|
|
23
|
+
* `fallbacks`.
|
|
24
|
+
*/
|
|
25
|
+
explicit?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Fuzzy-match an `explicit` name against the live catalogue so loose
|
|
28
|
+
* names like `"claude sonnet"` resolve. Default `true`. When `false`
|
|
29
|
+
* the explicit input is returned verbatim (Databricks surfaces the
|
|
30
|
+
* canonical 404 if it doesn't exist).
|
|
31
|
+
*/
|
|
32
|
+
fuzzy?: boolean;
|
|
33
|
+
/** Fuse.js threshold forwarded to {@link resolveModelId}. */
|
|
34
|
+
threshold?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Capability tier to resolve when no `explicit` id is given. The
|
|
37
|
+
* live catalogue is classified by its Foundation Model API scores
|
|
38
|
+
* and the top available model in the tier wins, falling back to the
|
|
39
|
+
* tier's small static list.
|
|
40
|
+
*/
|
|
41
|
+
tier?: ModelTier;
|
|
42
|
+
/**
|
|
43
|
+
* Operator-supplied fallback ids tried *first* in the no-explicit,
|
|
44
|
+
* no-tier path (e.g. a regulated workspace pinned to an approved
|
|
45
|
+
* subset), ahead of the auto-classified catalogue.
|
|
46
|
+
*/
|
|
47
|
+
fallbacks?: readonly string[];
|
|
48
|
+
}
|
|
49
|
+
/** Outcome of {@link resolveModel}: the chosen id plus how it was reached. */
|
|
50
|
+
export interface ResolvedModelSelection {
|
|
51
|
+
modelId: string;
|
|
52
|
+
source: "explicit" | "fuzzy-match" | "tier" | "fallback";
|
|
53
|
+
}
|
|
54
|
+
/** Intent + catalogue knobs passed to {@link selectModel}. */
|
|
55
|
+
export interface SelectModelInput extends ResolveModelInput {
|
|
56
|
+
/** TTL override for the cached `/serving-endpoints` listing, in ms. */
|
|
57
|
+
ttlMs?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a model id for a workspace in one call: list its
|
|
61
|
+
* `/serving-endpoints` (cached) and run {@link resolveModel} over the
|
|
62
|
+
* result. This is the entry point for any consumer that holds a
|
|
63
|
+
* `WorkspaceClient` and just wants a usable model name - a Lakeflow
|
|
64
|
+
* job, a one-off script, or the Mastra plugin alike.
|
|
65
|
+
*
|
|
66
|
+
* Cheap exit: when an `explicit` name is given and `fuzzy` is off, the
|
|
67
|
+
* catalogue is never fetched - the name is returned verbatim and
|
|
68
|
+
* Databricks surfaces the canonical 404 if it doesn't exist. Catalogue
|
|
69
|
+
* fetches otherwise fail loud: network / auth errors propagate so the
|
|
70
|
+
* caller sees the real SDK message instead of a silent fallback.
|
|
71
|
+
*
|
|
72
|
+
* @param host - Workspace host used as the cache key. Pass the value
|
|
73
|
+
* resolved from `client.config.getHost()`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function selectModel(client: WorkspaceClientLike, host: string, input?: SelectModelInput): Promise<ResolvedModelSelection>;
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a model id from the live catalogue and caller intent.
|
|
78
|
+
*
|
|
79
|
+
* 1. **Explicit ask**: with `fuzzy` off, returned verbatim; otherwise
|
|
80
|
+
* snapped to the closest live endpoint via {@link resolveModelId}.
|
|
81
|
+
* 2. **Tier intent**: classify the live catalogue and return the top
|
|
82
|
+
* available model in that tier, then the tier's static list, then
|
|
83
|
+
* the {@link FALLBACK_MODEL_IDS} floor.
|
|
84
|
+
* 3. **Neither**: walk `fallbacks` first, then the live
|
|
85
|
+
* score-classified catalogue (Thinking -> Balanced -> Fast), then
|
|
86
|
+
* the static floor, and return the first id actually present.
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveModel(endpoints: readonly ServingEndpointSummary[], input?: ResolveModelInput): ResolvedModelSelection;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-aware model selection.
|
|
3
|
+
*
|
|
4
|
+
* Given a caller's intent (an explicit name, a capability
|
|
5
|
+
* {@link ModelTier}, or nothing), the resolver returns the endpoint id
|
|
6
|
+
* to use - always one the workspace actually has, degrading from "best
|
|
7
|
+
* in tier" down to the static fallback floor. Two entry points:
|
|
8
|
+
*
|
|
9
|
+
* - {@link selectModel} is the high-level, I/O-doing call: hand it a
|
|
10
|
+
* `WorkspaceClient` and intent and it lists `/serving-endpoints`
|
|
11
|
+
* (cached) and resolves in one step. This is what a non-Mastra
|
|
12
|
+
* consumer (e.g. a job that just needs a model name) reaches for.
|
|
13
|
+
* - {@link resolveModel} is the pure form over an endpoint list you
|
|
14
|
+
* already hold; `selectModel` is a thin wrapper over it.
|
|
15
|
+
*/
|
|
16
|
+
import { classifyEndpoints, ModelTier, } from "@dbx-tools/model-shared";
|
|
17
|
+
import { FALLBACK_MODEL_IDS, modelsForTier } from "./fallback.js";
|
|
18
|
+
import { listServingEndpoints, resolveModelId, } from "./serving.js";
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a model id for a workspace in one call: list its
|
|
21
|
+
* `/serving-endpoints` (cached) and run {@link resolveModel} over the
|
|
22
|
+
* result. This is the entry point for any consumer that holds a
|
|
23
|
+
* `WorkspaceClient` and just wants a usable model name - a Lakeflow
|
|
24
|
+
* job, a one-off script, or the Mastra plugin alike.
|
|
25
|
+
*
|
|
26
|
+
* Cheap exit: when an `explicit` name is given and `fuzzy` is off, the
|
|
27
|
+
* catalogue is never fetched - the name is returned verbatim and
|
|
28
|
+
* Databricks surfaces the canonical 404 if it doesn't exist. Catalogue
|
|
29
|
+
* fetches otherwise fail loud: network / auth errors propagate so the
|
|
30
|
+
* caller sees the real SDK message instead of a silent fallback.
|
|
31
|
+
*
|
|
32
|
+
* @param host - Workspace host used as the cache key. Pass the value
|
|
33
|
+
* resolved from `client.config.getHost()`.
|
|
34
|
+
*/
|
|
35
|
+
export async function selectModel(client, host, input = {}) {
|
|
36
|
+
if (input.explicit !== undefined && input.fuzzy === false) {
|
|
37
|
+
return { modelId: input.explicit, source: "explicit" };
|
|
38
|
+
}
|
|
39
|
+
const endpoints = await listServingEndpoints(client, host, {
|
|
40
|
+
...(input.ttlMs !== undefined ? { ttlMs: input.ttlMs } : {}),
|
|
41
|
+
});
|
|
42
|
+
return resolveModel(endpoints, input);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a model id from the live catalogue and caller intent.
|
|
46
|
+
*
|
|
47
|
+
* 1. **Explicit ask**: with `fuzzy` off, returned verbatim; otherwise
|
|
48
|
+
* snapped to the closest live endpoint via {@link resolveModelId}.
|
|
49
|
+
* 2. **Tier intent**: classify the live catalogue and return the top
|
|
50
|
+
* available model in that tier, then the tier's static list, then
|
|
51
|
+
* the {@link FALLBACK_MODEL_IDS} floor.
|
|
52
|
+
* 3. **Neither**: walk `fallbacks` first, then the live
|
|
53
|
+
* score-classified catalogue (Thinking -> Balanced -> Fast), then
|
|
54
|
+
* the static floor, and return the first id actually present.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveModel(endpoints, input = {}) {
|
|
57
|
+
if (input.explicit !== undefined) {
|
|
58
|
+
if (input.fuzzy === false) {
|
|
59
|
+
return { modelId: input.explicit, source: "explicit" };
|
|
60
|
+
}
|
|
61
|
+
const { modelId } = resolveModelId(input.explicit, endpoints, {
|
|
62
|
+
threshold: input.threshold,
|
|
63
|
+
});
|
|
64
|
+
return { modelId, source: "fuzzy-match" };
|
|
65
|
+
}
|
|
66
|
+
const classified = classifyEndpoints(endpoints);
|
|
67
|
+
const chain = input.tier !== undefined
|
|
68
|
+
? tierChain(classified, input.tier)
|
|
69
|
+
: defaultChain(classified, input.fallbacks);
|
|
70
|
+
return {
|
|
71
|
+
modelId: pickFirstAvailable(chain, endpoints),
|
|
72
|
+
source: input.tier !== undefined ? "tier" : "fallback",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Candidate chain for an explicit {@link ModelTier} ask: live models
|
|
77
|
+
* classified into that tier (best-first), then the tier's small static
|
|
78
|
+
* list, then the full fallback floor as a last resort.
|
|
79
|
+
*/
|
|
80
|
+
function tierChain(classified, tier) {
|
|
81
|
+
return dedupe([
|
|
82
|
+
...classified[tier].map((e) => e.name),
|
|
83
|
+
...modelsForTier(tier),
|
|
84
|
+
...FALLBACK_MODEL_IDS,
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Candidate chain for the no-explicit default: any operator-supplied
|
|
89
|
+
* `fallbacks` first, then the live score-classified catalogue in
|
|
90
|
+
* descending tier order, then the static fallback floor.
|
|
91
|
+
*/
|
|
92
|
+
function defaultChain(classified, fallbacks) {
|
|
93
|
+
const live = [
|
|
94
|
+
...classified[ModelTier.Thinking],
|
|
95
|
+
...classified[ModelTier.Balanced],
|
|
96
|
+
...classified[ModelTier.Fast],
|
|
97
|
+
].map((e) => e.name);
|
|
98
|
+
return dedupe([...(fallbacks ?? []), ...live, ...FALLBACK_MODEL_IDS]);
|
|
99
|
+
}
|
|
100
|
+
/** Drop duplicate ids while preserving first-seen order. */
|
|
101
|
+
function dedupe(ids) {
|
|
102
|
+
return [...new Set(ids)];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Find the first id in `candidates` whose endpoint is present in
|
|
106
|
+
* `endpoints`. Returns the top candidate when the workspace has none
|
|
107
|
+
* of them so callers always get a string; an offline workspace then
|
|
108
|
+
* receives a clean 404 from Databricks instead of a malformed config.
|
|
109
|
+
*/
|
|
110
|
+
function pickFirstAvailable(candidates, endpoints) {
|
|
111
|
+
const present = new Set(endpoints.map((e) => e.name));
|
|
112
|
+
for (const candidate of candidates) {
|
|
113
|
+
if (present.has(candidate))
|
|
114
|
+
return candidate;
|
|
115
|
+
}
|
|
116
|
+
return candidates[0] ?? FALLBACK_MODEL_IDS[0];
|
|
117
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Databricks Model Serving catalogue access.
|
|
3
|
+
*
|
|
4
|
+
* Lists the workspace's `/serving-endpoints` once per host and caches
|
|
5
|
+
* the result with a TTL via AppKit's `CacheManager`, with concurrent
|
|
6
|
+
* callers sharing one in-flight promise (the coalescing pattern of
|
|
7
|
+
* Python's `cachetools-async`). Surfaces each endpoint as a stable
|
|
8
|
+
* {@link ServingEndpointSummary} - including the Foundation Model API
|
|
9
|
+
* `quality` / `speed` / `cost` profile when present - and snaps loose,
|
|
10
|
+
* human-typed names to real endpoint ids through `fuse.js` extended
|
|
11
|
+
* search so tokens like `"claude sonnet"` resolve to
|
|
12
|
+
* `databricks-claude-sonnet-4-6`.
|
|
13
|
+
*/
|
|
14
|
+
import { type getExecutionContext } from "@databricks/appkit";
|
|
15
|
+
import type { ServingEndpointSummary } from "@dbx-tools/model-shared";
|
|
16
|
+
/**
|
|
17
|
+
* Structural type for the Databricks workspace client. Derived from
|
|
18
|
+
* AppKit's `ExecutionContext` so this module doesn't take a direct
|
|
19
|
+
* dependency on `@databricks/sdk-experimental`; the dep flows in
|
|
20
|
+
* transitively through `@databricks/appkit`.
|
|
21
|
+
*/
|
|
22
|
+
export type WorkspaceClientLike = ReturnType<typeof getExecutionContext>["client"];
|
|
23
|
+
/** Default TTL for the in-memory endpoint cache. Matches the Databricks SDK's session lifetime budget. */
|
|
24
|
+
export declare const DEFAULT_MODEL_CACHE_TTL_MS: number;
|
|
25
|
+
/** Default Fuse.js score threshold below which a fuzzy match is accepted. */
|
|
26
|
+
export declare const DEFAULT_FUZZY_THRESHOLD = 0.4;
|
|
27
|
+
/**
|
|
28
|
+
* List Model Serving endpoints for the workspace owning `client`,
|
|
29
|
+
* routed through AppKit's `CacheManager`. The manager gives us
|
|
30
|
+
* everything `cachetools.TTLCache` provides plus what
|
|
31
|
+
* `cachetools-async` adds on top: per-entry TTL, in-flight request
|
|
32
|
+
* coalescing (concurrent callers share one fetch via the manager's
|
|
33
|
+
* internal `inFlightRequests` map), bounded size, telemetry spans
|
|
34
|
+
* (`cache.getOrExecute`), and optional Lakebase persistence so the
|
|
35
|
+
* catalogue survives restarts when the lakebase plugin is wired up.
|
|
36
|
+
*
|
|
37
|
+
* Returns plain {@link ServingEndpointSummary} objects (a stable
|
|
38
|
+
* subset of the SDK type) so cache hits never expose stale SDK
|
|
39
|
+
* internals. Errors from `CacheManager` or the SDK fetch propagate
|
|
40
|
+
* to the caller - we don't swallow them so users see the real
|
|
41
|
+
* auth / network issue.
|
|
42
|
+
*
|
|
43
|
+
* @param host - Workspace host used as the cache key. Pass the value
|
|
44
|
+
* resolved from `client.config.getHost()` so multi-host apps share
|
|
45
|
+
* one entry per workspace.
|
|
46
|
+
* @param opts.ttlMs - Override the default TTL just for this call.
|
|
47
|
+
* Forwarded to `CacheManager` as seconds.
|
|
48
|
+
*/
|
|
49
|
+
export declare function listServingEndpoints(client: WorkspaceClientLike, host: string, opts?: {
|
|
50
|
+
ttlMs?: number;
|
|
51
|
+
}): Promise<ServingEndpointSummary[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Force-evict cached endpoint listings via AppKit's `CacheManager`.
|
|
54
|
+
* With a `host` deletes that one workspace's entry; without one
|
|
55
|
+
* clears every cache entry on the manager (since `CacheManager`
|
|
56
|
+
* doesn't expose a namespace-scoped clear, this is the brute-force
|
|
57
|
+
* path - fine for tests, avoid in steady-state code).
|
|
58
|
+
*/
|
|
59
|
+
export declare function clearServingEndpointsCache(host?: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Result of fuzzy-resolving a user-supplied model name against the
|
|
62
|
+
* live endpoint list. `score` is Fuse.js's distance (`0` is exact,
|
|
63
|
+
* `1` is no match); `matched` is `false` when the score exceeds the
|
|
64
|
+
* configured threshold so callers can fall back to the original
|
|
65
|
+
* input (Databricks will then return a clean 404).
|
|
66
|
+
*/
|
|
67
|
+
export interface ResolvedModel {
|
|
68
|
+
modelId: string;
|
|
69
|
+
matched: boolean;
|
|
70
|
+
score?: number;
|
|
71
|
+
}
|
|
72
|
+
/** Options accepted by {@link resolveModelId}. */
|
|
73
|
+
export interface ResolveModelOptions {
|
|
74
|
+
/** Fuse.js threshold (0 = exact, 1 = anything). Default `0.4`. */
|
|
75
|
+
threshold?: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Snap a user-supplied model name to the closest configured serving
|
|
79
|
+
* endpoint:
|
|
80
|
+
*
|
|
81
|
+
* 1. Exact name match wins immediately (no fuzzy needed).
|
|
82
|
+
* 2. Otherwise the input is tokenized (dashes / underscores / spaces
|
|
83
|
+
* become separators) and fed through Fuse.js extended search,
|
|
84
|
+
* which AND-s each token with fuzzy matching enabled. This is the
|
|
85
|
+
* "tokenized fuzzy match" the user reaches for when they type
|
|
86
|
+
* `"claude sonnet"` instead of the full endpoint name.
|
|
87
|
+
* 3. If the best Fuse score is above `threshold`, return the input
|
|
88
|
+
* unchanged and let the upstream call surface the 404. This keeps
|
|
89
|
+
* deliberate model ids (e.g. brand new endpoints) from being
|
|
90
|
+
* silently rewritten to a similar-looking neighbour.
|
|
91
|
+
*
|
|
92
|
+
* Pass an empty endpoint list to short-circuit fuzzy matching - the
|
|
93
|
+
* input is returned verbatim. This is what callers do when the
|
|
94
|
+
* workspace client can't be reached at resolve time.
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveModelId(input: string, endpoints: readonly ServingEndpointSummary[], opts?: ResolveModelOptions): ResolvedModel;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Databricks Model Serving catalogue access.
|
|
3
|
+
*
|
|
4
|
+
* Lists the workspace's `/serving-endpoints` once per host and caches
|
|
5
|
+
* the result with a TTL via AppKit's `CacheManager`, with concurrent
|
|
6
|
+
* callers sharing one in-flight promise (the coalescing pattern of
|
|
7
|
+
* Python's `cachetools-async`). Surfaces each endpoint as a stable
|
|
8
|
+
* {@link ServingEndpointSummary} - including the Foundation Model API
|
|
9
|
+
* `quality` / `speed` / `cost` profile when present - and snaps loose,
|
|
10
|
+
* human-typed names to real endpoint ids through `fuse.js` extended
|
|
11
|
+
* search so tokens like `"claude sonnet"` resolve to
|
|
12
|
+
* `databricks-claude-sonnet-4-6`.
|
|
13
|
+
*/
|
|
14
|
+
import { CacheManager } from "@databricks/appkit";
|
|
15
|
+
import { logUtils, stringUtils } from "@dbx-tools/shared";
|
|
16
|
+
import Fuse from "fuse.js";
|
|
17
|
+
const log = logUtils.logger("model/serving");
|
|
18
|
+
/** Default TTL for the in-memory endpoint cache. Matches the Databricks SDK's session lifetime budget. */
|
|
19
|
+
export const DEFAULT_MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
20
|
+
/** Default Fuse.js score threshold below which a fuzzy match is accepted. */
|
|
21
|
+
export const DEFAULT_FUZZY_THRESHOLD = 0.4;
|
|
22
|
+
/** Cache key parts under which endpoint listings are stored. */
|
|
23
|
+
const CACHE_KEY_NAMESPACE = "serving-endpoints";
|
|
24
|
+
/**
|
|
25
|
+
* Stable `userKey` arg for AppKit's `CacheManager.getOrExecute`.
|
|
26
|
+
* Endpoint visibility is effectively workspace-scoped (we cache by
|
|
27
|
+
* host in the key parts), so a single shared key lets every user of
|
|
28
|
+
* the same workspace share one cached fetch and coalesce on the
|
|
29
|
+
* in-flight promise. Permissions can differ in theory, but the
|
|
30
|
+
* Foundation Model API catalogue is the same view for every caller.
|
|
31
|
+
*/
|
|
32
|
+
const SHARED_USER_KEY = "model-shared";
|
|
33
|
+
/**
|
|
34
|
+
* List Model Serving endpoints for the workspace owning `client`,
|
|
35
|
+
* routed through AppKit's `CacheManager`. The manager gives us
|
|
36
|
+
* everything `cachetools.TTLCache` provides plus what
|
|
37
|
+
* `cachetools-async` adds on top: per-entry TTL, in-flight request
|
|
38
|
+
* coalescing (concurrent callers share one fetch via the manager's
|
|
39
|
+
* internal `inFlightRequests` map), bounded size, telemetry spans
|
|
40
|
+
* (`cache.getOrExecute`), and optional Lakebase persistence so the
|
|
41
|
+
* catalogue survives restarts when the lakebase plugin is wired up.
|
|
42
|
+
*
|
|
43
|
+
* Returns plain {@link ServingEndpointSummary} objects (a stable
|
|
44
|
+
* subset of the SDK type) so cache hits never expose stale SDK
|
|
45
|
+
* internals. Errors from `CacheManager` or the SDK fetch propagate
|
|
46
|
+
* to the caller - we don't swallow them so users see the real
|
|
47
|
+
* auth / network issue.
|
|
48
|
+
*
|
|
49
|
+
* @param host - Workspace host used as the cache key. Pass the value
|
|
50
|
+
* resolved from `client.config.getHost()` so multi-host apps share
|
|
51
|
+
* one entry per workspace.
|
|
52
|
+
* @param opts.ttlMs - Override the default TTL just for this call.
|
|
53
|
+
* Forwarded to `CacheManager` as seconds.
|
|
54
|
+
*/
|
|
55
|
+
export async function listServingEndpoints(client, host, opts = {}) {
|
|
56
|
+
const ttlSec = Math.max(1, Math.round((opts.ttlMs ?? DEFAULT_MODEL_CACHE_TTL_MS) / 1000));
|
|
57
|
+
return CacheManager.getInstanceSync().getOrExecute([CACHE_KEY_NAMESPACE, host], () => fetchEndpoints(client), SHARED_USER_KEY, { ttl: ttlSec });
|
|
58
|
+
}
|
|
59
|
+
async function fetchEndpoints(client) {
|
|
60
|
+
const startedAt = Date.now();
|
|
61
|
+
const out = [];
|
|
62
|
+
for await (const ep of client.servingEndpoints.list()) {
|
|
63
|
+
if (!ep.name)
|
|
64
|
+
continue;
|
|
65
|
+
const profile = extractProfile(ep);
|
|
66
|
+
out.push({
|
|
67
|
+
name: ep.name,
|
|
68
|
+
...(ep.task !== undefined ? { task: ep.task } : {}),
|
|
69
|
+
...(ep.state?.ready !== undefined ? { state: String(ep.state.ready) } : {}),
|
|
70
|
+
...(ep.description !== undefined ? { description: ep.description } : {}),
|
|
71
|
+
...(profile ? { profile } : {}),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
log.debug("listed", { count: out.length, elapsedMs: Date.now() - startedAt });
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Pull the Foundation Model API `quality` / `speed` / `cost` scores
|
|
79
|
+
* off a serving-endpoint listing entry. Databricks returns these
|
|
80
|
+
* under `config.served_entities[].foundation_model.ai_gateway_model_profile`
|
|
81
|
+
* (snake_case at the wire level, preserved verbatim by the SDK), but
|
|
82
|
+
* the field is newer than the typed `FoundationModel` interface, so we
|
|
83
|
+
* read it through a structural cast. Returns `undefined` when no
|
|
84
|
+
* served entity carries a profile (custom models, embeddings, and
|
|
85
|
+
* brand-new endpoints that Databricks has not scored yet).
|
|
86
|
+
*/
|
|
87
|
+
function extractProfile(ep) {
|
|
88
|
+
const entities = ep.config?.served_entities;
|
|
89
|
+
if (!entities)
|
|
90
|
+
return undefined;
|
|
91
|
+
for (const entity of entities) {
|
|
92
|
+
const raw = entity.foundation_model?.ai_gateway_model_profile;
|
|
93
|
+
if (!raw)
|
|
94
|
+
continue;
|
|
95
|
+
const profile = {};
|
|
96
|
+
if (Number.isFinite(raw.quality))
|
|
97
|
+
profile.quality = raw.quality;
|
|
98
|
+
if (Number.isFinite(raw.speed))
|
|
99
|
+
profile.speed = raw.speed;
|
|
100
|
+
if (Number.isFinite(raw.cost))
|
|
101
|
+
profile.cost = raw.cost;
|
|
102
|
+
if (Object.keys(profile).length > 0)
|
|
103
|
+
return profile;
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Force-evict cached endpoint listings via AppKit's `CacheManager`.
|
|
109
|
+
* With a `host` deletes that one workspace's entry; without one
|
|
110
|
+
* clears every cache entry on the manager (since `CacheManager`
|
|
111
|
+
* doesn't expose a namespace-scoped clear, this is the brute-force
|
|
112
|
+
* path - fine for tests, avoid in steady-state code).
|
|
113
|
+
*/
|
|
114
|
+
export async function clearServingEndpointsCache(host) {
|
|
115
|
+
const cache = CacheManager.getInstanceSync();
|
|
116
|
+
if (host) {
|
|
117
|
+
const key = cache.generateKey([CACHE_KEY_NAMESPACE, host], SHARED_USER_KEY);
|
|
118
|
+
await cache.delete(key);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
await cache.clear();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Snap a user-supplied model name to the closest configured serving
|
|
126
|
+
* endpoint:
|
|
127
|
+
*
|
|
128
|
+
* 1. Exact name match wins immediately (no fuzzy needed).
|
|
129
|
+
* 2. Otherwise the input is tokenized (dashes / underscores / spaces
|
|
130
|
+
* become separators) and fed through Fuse.js extended search,
|
|
131
|
+
* which AND-s each token with fuzzy matching enabled. This is the
|
|
132
|
+
* "tokenized fuzzy match" the user reaches for when they type
|
|
133
|
+
* `"claude sonnet"` instead of the full endpoint name.
|
|
134
|
+
* 3. If the best Fuse score is above `threshold`, return the input
|
|
135
|
+
* unchanged and let the upstream call surface the 404. This keeps
|
|
136
|
+
* deliberate model ids (e.g. brand new endpoints) from being
|
|
137
|
+
* silently rewritten to a similar-looking neighbour.
|
|
138
|
+
*
|
|
139
|
+
* Pass an empty endpoint list to short-circuit fuzzy matching - the
|
|
140
|
+
* input is returned verbatim. This is what callers do when the
|
|
141
|
+
* workspace client can't be reached at resolve time.
|
|
142
|
+
*/
|
|
143
|
+
export function resolveModelId(input, endpoints, opts = {}) {
|
|
144
|
+
if (endpoints.length === 0) {
|
|
145
|
+
log.debug("resolve:no-endpoints", { input });
|
|
146
|
+
return { modelId: input, matched: false };
|
|
147
|
+
}
|
|
148
|
+
for (const ep of endpoints) {
|
|
149
|
+
if (ep.name === input) {
|
|
150
|
+
log.debug("resolve:exact", { input });
|
|
151
|
+
return { modelId: ep.name, matched: true, score: 0 };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const threshold = opts.threshold ?? DEFAULT_FUZZY_THRESHOLD;
|
|
155
|
+
const fuse = new Fuse(endpoints, {
|
|
156
|
+
keys: ["name"],
|
|
157
|
+
threshold,
|
|
158
|
+
ignoreLocation: true,
|
|
159
|
+
includeScore: true,
|
|
160
|
+
useExtendedSearch: true,
|
|
161
|
+
isCaseSensitive: false,
|
|
162
|
+
});
|
|
163
|
+
// Fuse 7.3 has no built-in tokenize hook; in extended search,
|
|
164
|
+
// space-separated tokens are AND-ed with fuzzy matching enabled. We
|
|
165
|
+
// lean on the shared tokenizer so the splitting rules stay
|
|
166
|
+
// consistent with the rest of the toolkit.
|
|
167
|
+
const query = Array.from(stringUtils.tokenizeWithOptions({ lowerCase: true, camelCase: false }, input)).join(" ");
|
|
168
|
+
if (!query) {
|
|
169
|
+
log.debug("resolve:empty-tokens", { input });
|
|
170
|
+
return { modelId: input, matched: false };
|
|
171
|
+
}
|
|
172
|
+
const results = fuse.search(query);
|
|
173
|
+
const best = results[0];
|
|
174
|
+
if (best?.item.name && (best.score ?? 0) <= threshold) {
|
|
175
|
+
log.debug("resolve:fuzzy-match", {
|
|
176
|
+
input,
|
|
177
|
+
modelId: best.item.name,
|
|
178
|
+
score: best.score,
|
|
179
|
+
});
|
|
180
|
+
return { modelId: best.item.name, matched: true, score: best.score };
|
|
181
|
+
}
|
|
182
|
+
log.debug("resolve:no-match", {
|
|
183
|
+
input,
|
|
184
|
+
bestScore: best?.score,
|
|
185
|
+
threshold,
|
|
186
|
+
});
|
|
187
|
+
return { modelId: input, matched: false };
|
|
188
|
+
}
|