@dbx-tools/model 0.9.5 → 0.9.6
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 +7 -7
- package/index.ts +4 -4
- package/lib/index.d.ts +4 -4
- package/lib/index.js +4 -4
- package/lib/src/_native.d.ts +14 -0
- package/lib/src/_native.js +124 -0
- package/lib/src/classes.d.ts +6 -1
- package/lib/src/fallback.d.ts +9 -6
- package/lib/src/fallback.js +14 -12
- package/lib/src/{serving.d.ts → model-catalog.d.ts} +25 -20
- package/lib/src/model-catalog.js +198 -0
- package/lib/src/policy.d.ts +5 -1
- package/lib/src/policy.js +14 -3
- package/lib/src/resolve.d.ts +8 -4
- package/lib/src/resolve.js +15 -71
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -6
- package/src/_native.ts +174 -0
- package/src/fallback.ts +15 -11
- package/src/{serving.ts → model-catalog.ts} +60 -156
- package/src/policy.ts +21 -2
- package/src/resolve.ts +19 -87
- package/lib/src/serving.js +0 -292
package/package.json
CHANGED
|
@@ -25,11 +25,10 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@databricks/appkit": "0.81.0",
|
|
28
|
-
"@dbx-tools/appkit": "^0.9.
|
|
29
|
-
"@dbx-tools/model-rs": "^0.9.
|
|
30
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
31
|
-
"@dbx-tools/shared-model": "^0.9.
|
|
32
|
-
"fuse.js": "^7.4.2"
|
|
28
|
+
"@dbx-tools/appkit": "^0.9.6",
|
|
29
|
+
"@dbx-tools/model-rs": "^0.9.6",
|
|
30
|
+
"@dbx-tools/shared-core": "^0.9.6",
|
|
31
|
+
"@dbx-tools/shared-model": "^0.9.6"
|
|
33
32
|
},
|
|
34
33
|
"main": "./lib/index.js",
|
|
35
34
|
"license": "Apache-2.0",
|
|
@@ -45,7 +44,7 @@
|
|
|
45
44
|
"./package.json": "./package.json"
|
|
46
45
|
}
|
|
47
46
|
},
|
|
48
|
-
"version": "0.9.
|
|
47
|
+
"version": "0.9.6",
|
|
49
48
|
"types": "./lib/index.d.ts",
|
|
50
49
|
"type": "module",
|
|
51
50
|
"exports": {
|
package/src/_native.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ModelClass as NativeModelClass,
|
|
3
|
+
ReasoningEffort as NativeReasoningEffort,
|
|
4
|
+
normalizeServingEndpointsJson,
|
|
5
|
+
rankModels as rankModelsWithRust,
|
|
6
|
+
type ModelQuery as NativeModelQuery,
|
|
7
|
+
type ServingEndpointSummary as NativeServingEndpointSummary,
|
|
8
|
+
} from "@dbx-tools/model-rs";
|
|
9
|
+
import {
|
|
10
|
+
model,
|
|
11
|
+
type ModelQuery,
|
|
12
|
+
type RankedModel,
|
|
13
|
+
type ReasoningEffort,
|
|
14
|
+
type ServingEndpointSummary,
|
|
15
|
+
} from "@dbx-tools/shared-model";
|
|
16
|
+
|
|
17
|
+
type ModelClass = model.ModelClass;
|
|
18
|
+
|
|
19
|
+
const MODEL_CLASS_TO_NATIVE: Readonly<Record<ModelClass, NativeModelClass>> = {
|
|
20
|
+
[model.ModelClass.ChatThinking]: NativeModelClass.ChatThinking,
|
|
21
|
+
[model.ModelClass.ChatBalanced]: NativeModelClass.ChatBalanced,
|
|
22
|
+
[model.ModelClass.ChatFast]: NativeModelClass.ChatFast,
|
|
23
|
+
[model.ModelClass.Embedding]: NativeModelClass.Embedding,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const REASONING_EFFORT_TO_NATIVE: Readonly<Record<string, NativeReasoningEffort>> = {
|
|
27
|
+
none: NativeReasoningEffort.None,
|
|
28
|
+
minimal: NativeReasoningEffort.Minimal,
|
|
29
|
+
low: NativeReasoningEffort.Low,
|
|
30
|
+
medium: NativeReasoningEffort.Medium,
|
|
31
|
+
high: NativeReasoningEffort.High,
|
|
32
|
+
xhigh: NativeReasoningEffort.Xhigh,
|
|
33
|
+
max: NativeReasoningEffort.Max,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const REASONING_EFFORT_FROM_NATIVE: Readonly<Record<NativeReasoningEffort, ReasoningEffort>> = {
|
|
37
|
+
[NativeReasoningEffort.None]: "none",
|
|
38
|
+
[NativeReasoningEffort.Minimal]: "minimal",
|
|
39
|
+
[NativeReasoningEffort.Low]: "low",
|
|
40
|
+
[NativeReasoningEffort.Medium]: "medium",
|
|
41
|
+
[NativeReasoningEffort.High]: "high",
|
|
42
|
+
[NativeReasoningEffort.Xhigh]: "xhigh",
|
|
43
|
+
[NativeReasoningEffort.Max]: "max",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
interface NativeRankingOptions {
|
|
47
|
+
readonly includeDeprecated?: boolean;
|
|
48
|
+
readonly modelClass?: ModelClass;
|
|
49
|
+
readonly task?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Rank public endpoint records through the Rust-owned model policy. */
|
|
53
|
+
export function rankEndpoints(
|
|
54
|
+
endpoints: readonly ServingEndpointSummary[],
|
|
55
|
+
query: ModelQuery = {},
|
|
56
|
+
options: NativeRankingOptions = {},
|
|
57
|
+
): RankedModel[] {
|
|
58
|
+
const endpointsByName = new Map(endpoints.map((endpoint) => [endpoint.name, endpoint]));
|
|
59
|
+
return rankModelsWithRust(
|
|
60
|
+
endpoints.map((endpoint) => toNativeEndpoint(endpoint, options)),
|
|
61
|
+
toNativeQuery(query, options.includeDeprecated ?? false),
|
|
62
|
+
).map((ranked) => {
|
|
63
|
+
const endpoint = endpointsByName.get(ranked.endpoint.name);
|
|
64
|
+
if (!endpoint) {
|
|
65
|
+
throw new Error(`Rust model ranking returned unknown endpoint "${ranked.endpoint.name}"`);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
endpoint,
|
|
69
|
+
modelClass: fromNativeClass(ranked.modelClass),
|
|
70
|
+
...(ranked.score !== undefined ? { score: ranked.score } : {}),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Classify chat and embedding endpoints through the Rust-owned policy. */
|
|
76
|
+
export function classifyEndpointClasses(
|
|
77
|
+
endpoints: readonly ServingEndpointSummary[],
|
|
78
|
+
): ReadonlyMap<string, ModelClass> {
|
|
79
|
+
const chat = rankEndpoints(endpoints, {}, { includeDeprecated: true });
|
|
80
|
+
const embeddings = rankEndpoints(
|
|
81
|
+
endpoints,
|
|
82
|
+
{ modelClass: model.ModelClass.Embedding },
|
|
83
|
+
{ includeDeprecated: true },
|
|
84
|
+
);
|
|
85
|
+
return new Map(
|
|
86
|
+
[...chat, ...embeddings].map((ranked) => [ranked.endpoint.name, ranked.modelClass]),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Normalize serialized SDK endpoint records through the Rust-owned catalogue policy. */
|
|
91
|
+
export function normalizeEndpoints(endpoints: readonly unknown[]): ServingEndpointSummary[] {
|
|
92
|
+
return normalizeServingEndpointsJson(JSON.stringify({ endpoints })).map(fromNativeEndpoint);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toNativeQuery(query: ModelQuery, includeDeprecated: boolean): NativeModelQuery {
|
|
96
|
+
return {
|
|
97
|
+
search: query.search,
|
|
98
|
+
modelClass:
|
|
99
|
+
query.modelClass === undefined ? undefined : MODEL_CLASS_TO_NATIVE[query.modelClass],
|
|
100
|
+
requiresTools: query.requiresTools,
|
|
101
|
+
includeDeprecated: includeDeprecated || undefined,
|
|
102
|
+
limit: query.limit,
|
|
103
|
+
threshold: query.threshold,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toNativeEndpoint(
|
|
108
|
+
endpoint: ServingEndpointSummary,
|
|
109
|
+
options: NativeRankingOptions,
|
|
110
|
+
): NativeServingEndpointSummary {
|
|
111
|
+
const modelClass = options.modelClass ?? endpoint.class;
|
|
112
|
+
return {
|
|
113
|
+
name: endpoint.name,
|
|
114
|
+
displayName: endpoint.displayName,
|
|
115
|
+
family: endpoint.family,
|
|
116
|
+
task: options.task ?? endpoint.task,
|
|
117
|
+
state: endpoint.state,
|
|
118
|
+
description: endpoint.description,
|
|
119
|
+
supportsTools: endpoint.supportsTools,
|
|
120
|
+
profile: endpoint.profile,
|
|
121
|
+
modelClass: modelClass === undefined ? undefined : MODEL_CLASS_TO_NATIVE[modelClass],
|
|
122
|
+
serviceNames: new Map(Object.entries(endpoint.serviceNames ?? {})),
|
|
123
|
+
modelServiceName: endpoint.modelServiceName,
|
|
124
|
+
reasoningEfforts: (endpoint.reasoningEfforts ?? []).map((effort) => {
|
|
125
|
+
const native = REASONING_EFFORT_TO_NATIVE[effort];
|
|
126
|
+
if (native === undefined) throw new Error(`Unknown reasoning effort "${effort}"`);
|
|
127
|
+
return native;
|
|
128
|
+
}),
|
|
129
|
+
status: { deprecated: endpoint.status?.deprecated ?? false },
|
|
130
|
+
dimension: endpoint.dimension,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function fromNativeEndpoint(endpoint: NativeServingEndpointSummary): ServingEndpointSummary {
|
|
135
|
+
return {
|
|
136
|
+
name: endpoint.name,
|
|
137
|
+
...(endpoint.displayName !== undefined ? { displayName: endpoint.displayName } : {}),
|
|
138
|
+
...(endpoint.family !== undefined ? { family: endpoint.family } : {}),
|
|
139
|
+
...(endpoint.task !== undefined ? { task: endpoint.task } : {}),
|
|
140
|
+
...(endpoint.state !== undefined ? { state: endpoint.state } : {}),
|
|
141
|
+
...(endpoint.description !== undefined ? { description: endpoint.description } : {}),
|
|
142
|
+
...(endpoint.supportsTools !== undefined ? { supportsTools: endpoint.supportsTools } : {}),
|
|
143
|
+
...(endpoint.profile !== undefined ? { profile: endpoint.profile } : {}),
|
|
144
|
+
...(endpoint.modelClass !== undefined ? { class: fromNativeClass(endpoint.modelClass) } : {}),
|
|
145
|
+
...(endpoint.serviceNames.size > 0
|
|
146
|
+
? { serviceNames: Object.fromEntries(endpoint.serviceNames) }
|
|
147
|
+
: {}),
|
|
148
|
+
...(endpoint.modelServiceName !== undefined
|
|
149
|
+
? { modelServiceName: endpoint.modelServiceName }
|
|
150
|
+
: {}),
|
|
151
|
+
...(endpoint.reasoningEfforts.length > 0
|
|
152
|
+
? {
|
|
153
|
+
reasoningEfforts: endpoint.reasoningEfforts.map(
|
|
154
|
+
(effort) => REASONING_EFFORT_FROM_NATIVE[effort],
|
|
155
|
+
),
|
|
156
|
+
}
|
|
157
|
+
: {}),
|
|
158
|
+
status: { deprecated: endpoint.status.deprecated },
|
|
159
|
+
...(endpoint.dimension !== undefined ? { dimension: endpoint.dimension } : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function fromNativeClass(modelClass: NativeModelClass): ModelClass {
|
|
164
|
+
switch (modelClass) {
|
|
165
|
+
case NativeModelClass.ChatThinking:
|
|
166
|
+
return model.ModelClass.ChatThinking;
|
|
167
|
+
case NativeModelClass.ChatBalanced:
|
|
168
|
+
return model.ModelClass.ChatBalanced;
|
|
169
|
+
case NativeModelClass.ChatFast:
|
|
170
|
+
return model.ModelClass.ChatFast;
|
|
171
|
+
case NativeModelClass.Embedding:
|
|
172
|
+
return model.ModelClass.Embedding;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/fallback.ts
CHANGED
|
@@ -5,27 +5,27 @@
|
|
|
5
5
|
* token, the service principal can't list, or the workspace is unreachable -
|
|
6
6
|
* the resolver still has to name *some* endpoint. This module holds that floor:
|
|
7
7
|
* a small, hard-coded set of well-known Foundation Model API endpoint names,
|
|
8
|
-
* each bucketed into a chat {@link ModelClass} by the
|
|
9
|
-
* {@link classify.classifyByFamily} heuristic (no classes are hard-coded here)
|
|
8
|
+
* each bucketed into a chat {@link ModelClass} by the Rust family policy
|
|
10
9
|
* and ordered best-first.
|
|
11
10
|
*
|
|
12
11
|
* This is deliberately *server-only*. A browser client never talks to
|
|
13
12
|
* Databricks directly - it always goes through this server - so it has nothing
|
|
14
13
|
* to fall back to and must not assume a stale, baked-in model list; it consumes
|
|
15
|
-
* the live `/models` response instead.
|
|
16
|
-
* ({@link classify.classifyEndpoints}) is what the client shares.
|
|
14
|
+
* the classified live `/models` response instead.
|
|
17
15
|
*
|
|
18
16
|
* @module
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
|
-
import {
|
|
19
|
+
import { model, type ServingEndpointSummary } from "@dbx-tools/shared-model";
|
|
20
|
+
|
|
21
|
+
import { rankEndpoints } from "./_native.ts";
|
|
22
22
|
|
|
23
23
|
type ModelClass = model.ModelClass;
|
|
24
24
|
const { ModelClass } = model;
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Small, last-resort set of well-known Foundation Model API endpoint names,
|
|
28
|
-
* ordered best-first within each class by
|
|
28
|
+
* ordered best-first within each class by the Rust model ranker.
|
|
29
29
|
* Used only as the floor when the live `/serving-endpoints` catalogue can't be
|
|
30
30
|
* read at resolve time; the live, score-driven classification supersedes it
|
|
31
31
|
* whenever the workspace listing is available. Classes are not hard-coded here
|
|
@@ -43,19 +43,23 @@ const FALLBACK_MODEL_NAMES: readonly string[] = [
|
|
|
43
43
|
"databricks-meta-llama-3-1-8b-instruct",
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
+
const FALLBACK_ENDPOINTS: readonly ServingEndpointSummary[] = FALLBACK_MODEL_NAMES.map((name) => ({
|
|
47
|
+
name,
|
|
48
|
+
task: "llm/v1/chat",
|
|
49
|
+
}));
|
|
50
|
+
|
|
46
51
|
/**
|
|
47
52
|
* Static fallback model ids for a chat class, drawn from the small built-in
|
|
48
53
|
* {@link FALLBACK_MODEL_NAMES} list and ordered best-first by family rank. Sync
|
|
49
54
|
* and workspace-independent: this is the *fallback opinion* used to seed default
|
|
50
55
|
* lists or when the live catalogue is unreachable - live resolution prefers
|
|
51
|
-
*
|
|
56
|
+
* the Rust-classified workspace catalogue. Returns `[]` for
|
|
52
57
|
* {@link ModelClass.Embedding} (the floor is chat-only).
|
|
53
58
|
*/
|
|
54
59
|
export function modelsForClass(cls: ModelClass): readonly string[] {
|
|
55
|
-
return
|
|
56
|
-
.filter((
|
|
57
|
-
.
|
|
58
|
-
.map((x) => x.name);
|
|
60
|
+
return rankEndpoints(FALLBACK_ENDPOINTS, { modelClass: cls })
|
|
61
|
+
.filter((ranked) => ranked.modelClass === cls)
|
|
62
|
+
.map((ranked) => ranked.endpoint.name);
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
/** Top static fallback model id for a chat class. */
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live Databricks Model Serving catalogue access.
|
|
3
3
|
*
|
|
4
|
-
* Lists the workspace's `/serving-endpoints`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Lists the workspace's `/serving-endpoints` and, when the caller supplies a
|
|
5
|
+
* trusted credential identity, caches the result per host and identity with a
|
|
6
|
+
* TTL via AppKit's `CacheManager`. Concurrent callers in the same scope share
|
|
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
|
|
10
10
|
* {@link ModelClass}, and (for embedding endpoints) the measured vector
|
|
11
11
|
* `dimension` - and snaps loose, human-typed names to real endpoint ids
|
|
12
|
-
* through
|
|
12
|
+
* through the Rust model ranker so tokens like `"claude sonnet"` resolve to
|
|
13
13
|
* `databricks-claude-sonnet-4-6`.
|
|
14
14
|
*
|
|
15
15
|
* The class stamp and embedding dimension are computed once per cache load:
|
|
@@ -21,20 +21,13 @@
|
|
|
21
21
|
* @module
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
24
25
|
import { CacheManager } from "@databricks/appkit";
|
|
25
26
|
import { appkit } from "@dbx-tools/appkit";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
classify,
|
|
29
|
-
display,
|
|
30
|
-
model,
|
|
31
|
-
type ModelProfile,
|
|
32
|
-
type ServingEndpointSummary,
|
|
33
|
-
} from "@dbx-tools/shared-model";
|
|
34
|
-
import Fuse from "fuse.js";
|
|
27
|
+
import { errorUtils, log } from "@dbx-tools/shared-core";
|
|
28
|
+
import { model, type ServingEndpointSummary } from "@dbx-tools/shared-model";
|
|
35
29
|
|
|
36
|
-
import {
|
|
37
|
-
import { modelFamily, modelReasoningEfforts } from "./policy.ts";
|
|
30
|
+
import { classifyEndpointClasses, normalizeEndpoints, rankEndpoints } from "./_native.ts";
|
|
38
31
|
|
|
39
32
|
const { ModelClass } = model;
|
|
40
33
|
|
|
@@ -50,22 +43,12 @@ export type WorkspaceClientLike = Pick<appkit.WorkspaceClientLike, "servingEndpo
|
|
|
50
43
|
/** Default TTL for the in-memory endpoint cache. Matches the Databricks SDK's session lifetime budget. */
|
|
51
44
|
export const DEFAULT_MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
52
45
|
|
|
53
|
-
/** Default
|
|
46
|
+
/** Default Rust distance threshold below which a fuzzy match is accepted. */
|
|
54
47
|
export const DEFAULT_FUZZY_THRESHOLD = 0.4;
|
|
55
48
|
|
|
56
49
|
/** Cache key parts under which endpoint listings are stored. */
|
|
57
50
|
const CACHE_KEY_NAMESPACE = "serving-endpoints";
|
|
58
51
|
|
|
59
|
-
/**
|
|
60
|
-
* Stable `userKey` arg for AppKit's `CacheManager.getOrExecute`. Endpoint
|
|
61
|
-
* visibility is effectively workspace-scoped (we cache by host in the key
|
|
62
|
-
* parts), so a single shared key lets every user of the same workspace share
|
|
63
|
-
* one cached fetch and coalesce on the in-flight promise. Permissions can
|
|
64
|
-
* differ in theory, but the Foundation Model API catalogue is the same view for
|
|
65
|
-
* every caller.
|
|
66
|
-
*/
|
|
67
|
-
const SHARED_USER_KEY = "model-shared";
|
|
68
|
-
|
|
69
52
|
/** Options for {@link listServingEndpoints}. */
|
|
70
53
|
export interface ListServingEndpointsOptions {
|
|
71
54
|
/**
|
|
@@ -73,6 +56,20 @@ export interface ListServingEndpointsOptions {
|
|
|
73
56
|
* `CacheManager` as seconds.
|
|
74
57
|
*/
|
|
75
58
|
ttlMs?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Trusted identity of the credential used by `client`.
|
|
61
|
+
*
|
|
62
|
+
* The value is hashed before it enters the cache key and is never logged.
|
|
63
|
+
* Omit it when the caller cannot prove the credential identity; that call
|
|
64
|
+
* bypasses shared caching so one principal can never read another's
|
|
65
|
+
* catalogue.
|
|
66
|
+
*/
|
|
67
|
+
cacheIdentity?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Hash an opaque credential identity before handing it to CacheManager. */
|
|
71
|
+
function cacheUserKey(identity: string): string {
|
|
72
|
+
return `model-${createHash("sha256").update(identity).digest("hex")}`;
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
/**
|
|
@@ -100,11 +97,12 @@ export async function listServingEndpoints(
|
|
|
100
97
|
host: string,
|
|
101
98
|
options: ListServingEndpointsOptions = {},
|
|
102
99
|
): Promise<ServingEndpointSummary[]> {
|
|
100
|
+
if (!options.cacheIdentity) return fetchEndpoints(client);
|
|
103
101
|
const ttlSec = Math.max(1, Math.round((options.ttlMs ?? DEFAULT_MODEL_CACHE_TTL_MS) / 1000));
|
|
104
102
|
return CacheManager.getInstanceSync().getOrExecute(
|
|
105
103
|
[CACHE_KEY_NAMESPACE, host],
|
|
106
104
|
() => fetchEndpoints(client),
|
|
107
|
-
|
|
105
|
+
cacheUserKey(options.cacheIdentity),
|
|
108
106
|
{ ttl: ttlSec },
|
|
109
107
|
);
|
|
110
108
|
}
|
|
@@ -122,51 +120,11 @@ export async function listServingEndpoints(
|
|
|
122
120
|
export async function listServingEndpointsUncached(
|
|
123
121
|
client: WorkspaceClientLike,
|
|
124
122
|
): Promise<ServingEndpointSummary[]> {
|
|
125
|
-
const
|
|
123
|
+
const endpoints: unknown[] = [];
|
|
126
124
|
for await (const ep of client.servingEndpoints.list()) {
|
|
127
|
-
|
|
128
|
-
const profile = extractProfile(ep);
|
|
129
|
-
const family = modelFamily(ep.name);
|
|
130
|
-
const reasoningEfforts = modelReasoningEfforts(ep.name);
|
|
131
|
-
out.push({
|
|
132
|
-
name: ep.name,
|
|
133
|
-
// Prefer a Databricks-provided human name (a display-name tag or an
|
|
134
|
-
// external-model name); else derive a title-cased label from the id.
|
|
135
|
-
displayName: display.toModelDisplayName(ep.name, providedDisplayName(ep)),
|
|
136
|
-
...(family !== undefined ? { family } : {}),
|
|
137
|
-
...(ep.task !== undefined ? { task: ep.task } : {}),
|
|
138
|
-
...(ep.state?.ready !== undefined ? { state: String(ep.state.ready) } : {}),
|
|
139
|
-
...(ep.description !== undefined ? { description: ep.description } : {}),
|
|
140
|
-
supportsTools: classify.supportsToolsByFamily(ep.name),
|
|
141
|
-
...(profile ? { profile } : {}),
|
|
142
|
-
...(reasoningEfforts.length > 0 ? { reasoningEfforts } : {}),
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
return out;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Pull a Databricks-provided human name off a serving endpoint, if any:
|
|
150
|
-
* a `display_name` / `displayName` / `name` tag first, then an
|
|
151
|
-
* external-model `name` from the served-entity config. Returns `null`
|
|
152
|
-
* when the endpoint carries no explicit name (the common case for
|
|
153
|
-
* Foundation Model API endpoints), so the caller falls back to deriving
|
|
154
|
-
* one from the id.
|
|
155
|
-
*/
|
|
156
|
-
function providedDisplayName(ep: {
|
|
157
|
-
tags?: Array<{ key: string; value?: string }>;
|
|
158
|
-
config?: { served_entities?: Array<{ external_model?: { name?: string } }> };
|
|
159
|
-
}): string | null {
|
|
160
|
-
const tag = ep.tags?.find(
|
|
161
|
-
(t) => t.key === "display_name" || t.key === "displayName" || t.key === "name",
|
|
162
|
-
);
|
|
163
|
-
const fromTag = string.trimToNull(tag?.value);
|
|
164
|
-
if (fromTag) return fromTag;
|
|
165
|
-
for (const entity of ep.config?.served_entities ?? []) {
|
|
166
|
-
const external = string.trimToNull(entity.external_model?.name);
|
|
167
|
-
if (external) return external;
|
|
125
|
+
endpoints.push(ep);
|
|
168
126
|
}
|
|
169
|
-
return
|
|
127
|
+
return normalizeEndpoints(endpoints);
|
|
170
128
|
}
|
|
171
129
|
|
|
172
130
|
async function fetchEndpoints(client: WorkspaceClientLike): Promise<ServingEndpointSummary[]> {
|
|
@@ -185,11 +143,7 @@ async function fetchEndpoints(client: WorkspaceClientLike): Promise<ServingEndpo
|
|
|
185
143
|
* class.
|
|
186
144
|
*/
|
|
187
145
|
function stampModelClasses(summaries: ServingEndpointSummary[]): void {
|
|
188
|
-
const
|
|
189
|
-
const classOf = new Map<string, model.ModelClass>();
|
|
190
|
-
for (const cls of MODEL_CLASS_ORDER) {
|
|
191
|
-
for (const ep of buckets[cls]) classOf.set(ep.name, cls);
|
|
192
|
-
}
|
|
146
|
+
const classOf = classifyEndpointClasses(summaries);
|
|
193
147
|
for (const summary of summaries) {
|
|
194
148
|
const cls = classOf.get(summary.name);
|
|
195
149
|
if (cls !== undefined) summary.class = cls;
|
|
@@ -235,61 +189,24 @@ async function pingEmbeddingDimension(
|
|
|
235
189
|
logger.warn("embedding ping returned no vector", { name });
|
|
236
190
|
return undefined;
|
|
237
191
|
} catch (err) {
|
|
238
|
-
logger.warn("embedding ping failed", { name, error:
|
|
192
|
+
logger.warn("embedding ping failed", { name, error: errorUtils.errorMessage(err) });
|
|
239
193
|
return undefined;
|
|
240
194
|
}
|
|
241
195
|
}
|
|
242
196
|
|
|
243
197
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* is newer than the typed `FoundationModel` interface, so we read it through a
|
|
249
|
-
* structural cast. Returns `undefined` when no served entity carries a profile
|
|
250
|
-
* (custom models, embeddings, and brand-new endpoints that Databricks has not
|
|
251
|
-
* scored yet).
|
|
198
|
+
* Force-evict cached endpoint listings via AppKit's `CacheManager`. A host and
|
|
199
|
+
* identity delete only that principal's workspace entry. Because CacheManager
|
|
200
|
+
* cannot enumerate a namespace, omitting either value clears the complete
|
|
201
|
+
* manager and should be reserved for tests or administrative refreshes.
|
|
252
202
|
*/
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
served_entities?: Array<{
|
|
258
|
-
foundation_model?: {
|
|
259
|
-
ai_gateway_model_profile?: {
|
|
260
|
-
quality?: number;
|
|
261
|
-
speed?: number;
|
|
262
|
-
cost?: number;
|
|
263
|
-
};
|
|
264
|
-
};
|
|
265
|
-
}>;
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
).config?.served_entities;
|
|
269
|
-
if (!entities) return undefined;
|
|
270
|
-
for (const entity of entities) {
|
|
271
|
-
const raw = entity.foundation_model?.ai_gateway_model_profile;
|
|
272
|
-
if (!raw) continue;
|
|
273
|
-
const profile: ModelProfile = {};
|
|
274
|
-
if (Number.isFinite(raw.quality)) profile.quality = raw.quality;
|
|
275
|
-
if (Number.isFinite(raw.speed)) profile.speed = raw.speed;
|
|
276
|
-
if (Number.isFinite(raw.cost)) profile.cost = raw.cost;
|
|
277
|
-
if (Object.keys(profile).length > 0) return profile;
|
|
278
|
-
}
|
|
279
|
-
return undefined;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Force-evict cached endpoint listings via AppKit's `CacheManager`. With a
|
|
284
|
-
* `host` deletes that one workspace's entry; without one clears every cache
|
|
285
|
-
* entry on the manager (since `CacheManager` doesn't expose a namespace-scoped
|
|
286
|
-
* clear, this is the brute-force path - fine for tests, avoid in steady-state
|
|
287
|
-
* code).
|
|
288
|
-
*/
|
|
289
|
-
export async function clearServingEndpointsCache(host?: string): Promise<void> {
|
|
203
|
+
export async function clearServingEndpointsCache(
|
|
204
|
+
host?: string,
|
|
205
|
+
cacheIdentity?: string,
|
|
206
|
+
): Promise<void> {
|
|
290
207
|
const cache = CacheManager.getInstanceSync();
|
|
291
|
-
if (host) {
|
|
292
|
-
const key = cache.generateKey([CACHE_KEY_NAMESPACE, host],
|
|
208
|
+
if (host && cacheIdentity) {
|
|
209
|
+
const key = cache.generateKey([CACHE_KEY_NAMESPACE, host], cacheUserKey(cacheIdentity));
|
|
293
210
|
await cache.delete(key);
|
|
294
211
|
} else {
|
|
295
212
|
await cache.clear();
|
|
@@ -298,7 +215,7 @@ export async function clearServingEndpointsCache(host?: string): Promise<void> {
|
|
|
298
215
|
|
|
299
216
|
/**
|
|
300
217
|
* Result of fuzzy-resolving a user-supplied model name against the live
|
|
301
|
-
* endpoint list. `score` is
|
|
218
|
+
* endpoint list. `score` is the Rust ranker's distance (`0` is exact, `1` is no match);
|
|
302
219
|
* `matched` is `false` when the score exceeds the configured threshold so
|
|
303
220
|
* callers can fall back to the original input (Databricks will then return a
|
|
304
221
|
* clean 404).
|
|
@@ -311,7 +228,7 @@ export interface ResolvedModel {
|
|
|
311
228
|
|
|
312
229
|
/** Options accepted by {@link resolveModelId} / {@link searchServingEndpoints}. */
|
|
313
230
|
export interface ResolveModelOptions {
|
|
314
|
-
/**
|
|
231
|
+
/** Fuzzy distance threshold (0 = exact, 1 = anything). Default `0.4`. */
|
|
315
232
|
threshold?: number;
|
|
316
233
|
/** Require a model verified for a complete function-tool round-trip. */
|
|
317
234
|
requiresTools?: boolean;
|
|
@@ -320,7 +237,7 @@ export interface ResolveModelOptions {
|
|
|
320
237
|
/** A serving endpoint paired with its fuzzy-match distance for a query. */
|
|
321
238
|
export interface ScoredEndpoint {
|
|
322
239
|
endpoint: ServingEndpointSummary;
|
|
323
|
-
/**
|
|
240
|
+
/** Fuzzy distance: `0` is exact, `1` is no match. */
|
|
324
241
|
score: number;
|
|
325
242
|
}
|
|
326
243
|
|
|
@@ -329,11 +246,8 @@ export interface ScoredEndpoint {
|
|
|
329
246
|
* (lowest score) first, keeping only those within `threshold`:
|
|
330
247
|
*
|
|
331
248
|
* 1. An exact name match short-circuits to a single `score: 0` result.
|
|
332
|
-
* 2. Otherwise the input
|
|
333
|
-
*
|
|
334
|
-
* token with fuzzy matching enabled - the "tokenized fuzzy match" a caller
|
|
335
|
-
* reaches for when they type `"claude sonnet"` instead of the full endpoint
|
|
336
|
-
* name.
|
|
249
|
+
* 2. Otherwise Rust tokenizes the input and scores every endpoint through the
|
|
250
|
+
* same fuzzy-distance policy used by {@link lookupModels}.
|
|
337
251
|
*
|
|
338
252
|
* Returns `[]` for an empty endpoint list or when `input` tokenizes to nothing,
|
|
339
253
|
* so callers fall back to the raw input and let Databricks surface a clean 404.
|
|
@@ -345,30 +259,20 @@ export function searchServingEndpoints(
|
|
|
345
259
|
endpoints: readonly ServingEndpointSummary[],
|
|
346
260
|
options: ResolveModelOptions = {},
|
|
347
261
|
): ScoredEndpoint[] {
|
|
348
|
-
if (endpoints.length === 0) return [];
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
threshold,
|
|
363
|
-
ignoreLocation: true,
|
|
364
|
-
includeScore: true,
|
|
365
|
-
useExtendedSearch: true,
|
|
366
|
-
isCaseSensitive: false,
|
|
367
|
-
});
|
|
368
|
-
return fuse
|
|
369
|
-
.search(query)
|
|
370
|
-
.filter((r) => (r.score ?? 0) <= threshold)
|
|
371
|
-
.map((r) => ({ endpoint: r.item, score: r.score ?? 0 }));
|
|
262
|
+
if (endpoints.length === 0 || !input.trim()) return [];
|
|
263
|
+
return rankEndpoints(
|
|
264
|
+
endpoints,
|
|
265
|
+
{
|
|
266
|
+
search: input,
|
|
267
|
+
...(options.requiresTools !== undefined ? { requiresTools: options.requiresTools } : {}),
|
|
268
|
+
threshold: options.threshold ?? DEFAULT_FUZZY_THRESHOLD,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
includeDeprecated: true,
|
|
272
|
+
modelClass: model.ModelClass.ChatBalanced,
|
|
273
|
+
task: "llm/v1/chat",
|
|
274
|
+
},
|
|
275
|
+
).map((ranked) => ({ endpoint: ranked.endpoint, score: ranked.score ?? 0 }));
|
|
372
276
|
}
|
|
373
277
|
|
|
374
278
|
/**
|
package/src/policy.ts
CHANGED
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
import {
|
|
8
8
|
modelFamily as modelFamilyWithRust,
|
|
9
9
|
reasoningEffortNamesByFamily,
|
|
10
|
+
supportsToolsByFamily as supportsToolsByFamilyWithRust,
|
|
10
11
|
} from "@dbx-tools/model-rs";
|
|
11
|
-
import
|
|
12
|
+
import {
|
|
13
|
+
model,
|
|
14
|
+
ReasoningEffortSchema,
|
|
15
|
+
type ReasoningEffort,
|
|
16
|
+
type ServingEndpointSummary,
|
|
17
|
+
} from "@dbx-tools/shared-model";
|
|
12
18
|
|
|
13
19
|
/** Return the normalized family parsed by the Rust model-name policy. */
|
|
14
20
|
export function modelFamily(name: string): string | undefined {
|
|
@@ -17,5 +23,18 @@ export function modelFamily(name: string): string | undefined {
|
|
|
17
23
|
|
|
18
24
|
/** Return the reasoning efforts accepted by a model family. */
|
|
19
25
|
export function modelReasoningEfforts(name: string): ReasoningEffort[] {
|
|
20
|
-
return reasoningEffortNamesByFamily(name);
|
|
26
|
+
return ReasoningEffortSchema.array().parse(reasoningEffortNamesByFamily(name));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Return whether Rust policy verifies a complete tool-calling round trip. */
|
|
30
|
+
export function modelSupportsTools(name: string): boolean {
|
|
31
|
+
return supportsToolsByFamilyWithRust(name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Return whether a discovered endpoint can be used for a tool-calling chat. */
|
|
35
|
+
export function endpointSupportsTools(endpoint: ServingEndpointSummary): boolean {
|
|
36
|
+
const embedding =
|
|
37
|
+
endpoint.task === "llm/v1/embeddings" || endpoint.class === model.ModelClass.Embedding;
|
|
38
|
+
const chat = !embedding && (endpoint.task === "llm/v1/chat" || endpoint.class !== undefined);
|
|
39
|
+
return chat && (endpoint.supportsTools ?? modelSupportsTools(endpoint.name));
|
|
21
40
|
}
|