@dbx-tools/model 0.3.21 → 0.3.23
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 +38 -1
- package/index.ts +2 -0
- package/package.json +4 -4
- package/src/invoke.ts +68 -0
- package/src/resolve.ts +58 -2
- package/src/serving.ts +2 -2
- package/test/tsconfig.json +14 -0
package/README.md
CHANGED
|
@@ -21,7 +21,10 @@ Key features:
|
|
|
21
21
|
endpoint id.
|
|
22
22
|
- Supports class ceilings so callers can ask for a capability band without
|
|
23
23
|
accidentally escalating to a larger model.
|
|
24
|
-
- Caches enriched catalogues per workspace host through AppKit cache utilities
|
|
24
|
+
- Caches enriched catalogues per workspace host through AppKit cache utilities,
|
|
25
|
+
and re-lists once on a resolve miss so a newly deployed model still resolves.
|
|
26
|
+
- Builds the invocations URL and mints per-request auth headers for callers that
|
|
27
|
+
issue their own OpenAI-shaped HTTP requests.
|
|
25
28
|
- Provides a small static fallback floor for local tools and degraded workspace
|
|
26
29
|
access.
|
|
27
30
|
|
|
@@ -137,6 +140,38 @@ Fuzzy matching is intentionally a server concern because it depends on the live
|
|
|
137
140
|
workspace catalogue and may re-list on misses. Disable it in callers that require
|
|
138
141
|
exact endpoint ids.
|
|
139
142
|
|
|
143
|
+
## Resolve Against A Catalogue That May Be Stale
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const resolved = await resolve.rankModelIdLive((force) => loadCatalogue(force), "opus");
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`rankModelIdLive()` matches the catalogue you already hold and, on a miss,
|
|
150
|
+
reloads once with `force` before giving up, so a model deployed after your cache
|
|
151
|
+
warmed still resolves without a restart. You supply the loader, so the caching
|
|
152
|
+
policy stays yours: `listServingEndpoints` and its `CacheManager`, a plain field
|
|
153
|
+
in a long-lived CLI, or a test double. `rankModelId()` is the pure form over a
|
|
154
|
+
single snapshot; unlike `serving.resolveModelId` it breaks equal match scores by
|
|
155
|
+
class and then version, so `"opus"` prefers `opus-5` over `opus-4-7`.
|
|
156
|
+
|
|
157
|
+
## Call An Endpoint Directly
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { invoke } from "@dbx-tools/model";
|
|
161
|
+
|
|
162
|
+
const response = await fetch(invoke.invocationsUrl(host, endpointId), {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { ...(await invoke.authHeaders(client)), "content-type": "application/json" },
|
|
165
|
+
body: JSON.stringify({ messages }),
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Use `invoke` when you need to issue your own request against a serving endpoint
|
|
170
|
+
with an OpenAI-shaped body - a proxy, a passthrough route, a streaming client -
|
|
171
|
+
rather than the SDK's typed `servingEndpoints.query`. Mint `authHeaders()` per
|
|
172
|
+
request: the SDK refreshes the underlying token as it nears expiry, so you never
|
|
173
|
+
track lifetimes yourself.
|
|
174
|
+
|
|
140
175
|
## Use Static Fallbacks
|
|
141
176
|
|
|
142
177
|
```ts
|
|
@@ -157,6 +192,8 @@ policy decisions; fallbacks are a last resort.
|
|
|
157
192
|
resolver functions.
|
|
158
193
|
- `serving` - Databricks serving-endpoint listing, cache management, fuzzy
|
|
159
194
|
search, and endpoint-id resolution.
|
|
195
|
+
- `invoke` - invocations URL construction and per-request auth headers for
|
|
196
|
+
calling an endpoint over raw HTTP.
|
|
160
197
|
- `classes` - model-class parsing, ordering, and class-ceiling helpers.
|
|
161
198
|
- `fallback` - static fallback model ids per class.
|
|
162
199
|
|
package/index.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
export * as classes from "./src/classes";
|
|
6
6
|
export * as fallback from "./src/fallback";
|
|
7
|
+
export * as invoke from "./src/invoke";
|
|
7
8
|
export * as resolve from "./src/resolve";
|
|
8
9
|
export * as serving from "./src/serving";
|
|
10
|
+
export type { AuthenticatingClientLike } from "./src/invoke";
|
|
9
11
|
export type { ResolveModelInput, ResolvedModelSelection, SelectModelInput, SearchModelsInput } from "./src/resolve";
|
|
10
12
|
export type { WorkspaceClientLike, ListServingEndpointsOptions, ResolvedModel, ResolveModelOptions, ScoredEndpoint } from "./src/serving";
|
package/package.json
CHANGED
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@databricks/appkit": "^0.43.0",
|
|
15
15
|
"fuse.js": "^7.4.2",
|
|
16
|
-
"@dbx-tools/
|
|
17
|
-
"@dbx-tools/shared-model": "0.3.
|
|
18
|
-
"@dbx-tools/
|
|
16
|
+
"@dbx-tools/shared-core": "0.3.23",
|
|
17
|
+
"@dbx-tools/shared-model": "0.3.23",
|
|
18
|
+
"@dbx-tools/appkit": "0.3.23"
|
|
19
19
|
},
|
|
20
20
|
"main": "index.ts",
|
|
21
21
|
"license": "UNLICENSED",
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.23",
|
|
26
26
|
"types": "index.ts",
|
|
27
27
|
"type": "module",
|
|
28
28
|
"exports": {
|
package/src/invoke.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calling a Model Serving endpoint directly over HTTP.
|
|
3
|
+
*
|
|
4
|
+
* The SDK's `servingEndpoints.query` covers the typed request shapes, but an
|
|
5
|
+
* OpenAI-compatible caller (a proxy, a passthrough route, a streaming client)
|
|
6
|
+
* needs to issue its own `fetch` against the endpoint's `invocations` URL with
|
|
7
|
+
* an OpenAI body. That needs exactly two things the SDK doesn't hand out
|
|
8
|
+
* directly: the URL ({@link invocationsUrl}) and a currently-valid set of auth
|
|
9
|
+
* headers ({@link authHeaders}).
|
|
10
|
+
*
|
|
11
|
+
* Auth is delegated entirely to the Databricks SDK: `config.authenticate`
|
|
12
|
+
* re-runs the configured credential provider on every call and refreshes the
|
|
13
|
+
* underlying OAuth / PAT token when it is close to expiry, so a caller never
|
|
14
|
+
* manages token lifetimes itself - mint headers per request and each one is
|
|
15
|
+
* signed with a valid bearer token.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Path segment appended to a serving endpoint for OpenAI-compatible requests.
|
|
22
|
+
* Databricks routes `/serving-endpoints/<name>/invocations` to the endpoint's
|
|
23
|
+
* OpenAI-shaped surface (`messages` in, `choices` out).
|
|
24
|
+
*/
|
|
25
|
+
export const INVOCATIONS_SUFFIX = "invocations";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The OpenAI-compatible invocations URL for an endpoint id.
|
|
29
|
+
*
|
|
30
|
+
* @param host - Workspace host, e.g. `https://my-workspace.cloud.databricks.com/`.
|
|
31
|
+
* Pass the value resolved from `client.config.getHost()`.
|
|
32
|
+
* @param endpoint - Endpoint id (already resolved; not fuzzy-matched here).
|
|
33
|
+
*/
|
|
34
|
+
export function invocationsUrl(host: string, endpoint: string): string {
|
|
35
|
+
return new URL(
|
|
36
|
+
`serving-endpoints/${encodeURIComponent(endpoint)}/${INVOCATIONS_SUFFIX}`,
|
|
37
|
+
host,
|
|
38
|
+
).toString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Minimal structural shape {@link authHeaders} needs. Declared narrowly rather
|
|
43
|
+
* than as a full `WorkspaceClientLike` so a caller holding its own
|
|
44
|
+
* `@databricks/sdk-experimental` `WorkspaceClient` satisfies it regardless of
|
|
45
|
+
* which SDK version resolved in its tree.
|
|
46
|
+
*/
|
|
47
|
+
export interface AuthenticatingClientLike {
|
|
48
|
+
config: {
|
|
49
|
+
authenticate(headers: Headers): Promise<void>;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mint auth headers for one upstream request as a plain object, ready to spread
|
|
55
|
+
* into a `fetch` init. Call it per request: the SDK refreshes the underlying
|
|
56
|
+
* token when needed, so a long-lived process never has to track expiry itself.
|
|
57
|
+
*/
|
|
58
|
+
export async function authHeaders(
|
|
59
|
+
client: AuthenticatingClientLike,
|
|
60
|
+
): Promise<Record<string, string>> {
|
|
61
|
+
const headers = new Headers();
|
|
62
|
+
await client.config.authenticate(headers);
|
|
63
|
+
const out: Record<string, string> = {};
|
|
64
|
+
headers.forEach((value, key) => {
|
|
65
|
+
out[key] = value;
|
|
66
|
+
});
|
|
67
|
+
return out;
|
|
68
|
+
}
|
package/src/resolve.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* @module
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { object } from "@dbx-tools/shared-core";
|
|
23
24
|
import {
|
|
24
25
|
classify,
|
|
25
26
|
model,
|
|
@@ -27,11 +28,16 @@ import {
|
|
|
27
28
|
type RankedModel,
|
|
28
29
|
type ServingEndpointSummary,
|
|
29
30
|
} from "@dbx-tools/shared-model";
|
|
30
|
-
import { object } from "@dbx-tools/shared-core";
|
|
31
31
|
|
|
32
32
|
import { CHAT_CLASS_ORDER, classesAtOrBelow, MODEL_CLASS_ORDER } from "./classes";
|
|
33
33
|
import { FALLBACK_MODEL_IDS, modelsForClass } from "./fallback";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
listServingEndpoints,
|
|
36
|
+
searchServingEndpoints,
|
|
37
|
+
type ResolvedModel,
|
|
38
|
+
type ResolveModelOptions,
|
|
39
|
+
type WorkspaceClientLike,
|
|
40
|
+
} from "./serving";
|
|
35
41
|
|
|
36
42
|
type ModelClass = model.ModelClass;
|
|
37
43
|
|
|
@@ -154,6 +160,56 @@ export function rankModels(
|
|
|
154
160
|
return query.limit !== undefined ? ranked.slice(0, Math.max(0, query.limit)) : ranked;
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Collapse {@link rankModels} to a single id: the closest endpoint to `search`
|
|
165
|
+
* in a catalogue snapshot, or the input verbatim when nothing scores within the
|
|
166
|
+
* threshold.
|
|
167
|
+
*
|
|
168
|
+
* The rank-based counterpart to the Fuse-only {@link resolveModelId}: equal
|
|
169
|
+
* match scores are broken by class and then within-class version, so a loose
|
|
170
|
+
* `"opus"` prefers `opus-5` over `opus-4-7` instead of picking whichever
|
|
171
|
+
* sibling Fuse happened to order first. Returning the input unmatched (rather
|
|
172
|
+
* than a near neighbour) is deliberate - a deliberate endpoint id is never
|
|
173
|
+
* silently rewritten, and Databricks surfaces a clean 404.
|
|
174
|
+
*/
|
|
175
|
+
export function rankModelId(
|
|
176
|
+
endpoints: readonly ServingEndpointSummary[],
|
|
177
|
+
search: string,
|
|
178
|
+
options: ResolveModelOptions = {},
|
|
179
|
+
): ResolvedModel {
|
|
180
|
+
const [top] = rankModels(endpoints, {
|
|
181
|
+
search,
|
|
182
|
+
limit: 1,
|
|
183
|
+
...(options.threshold !== undefined ? { threshold: options.threshold } : {}),
|
|
184
|
+
});
|
|
185
|
+
if (!top) return { modelId: search, matched: false };
|
|
186
|
+
return { modelId: top.endpoint.name, matched: true, score: top.score };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* {@link rankModelId} against a catalogue the caller may be holding stale:
|
|
191
|
+
* match the loaded snapshot, and on a miss reload once with `force` and match
|
|
192
|
+
* again. That way a model deployed after the catalogue was cached still
|
|
193
|
+
* resolves on first use, without a restart and without giving up caching.
|
|
194
|
+
*
|
|
195
|
+
* The catalogue arrives as a loader rather than a client so the caller keeps
|
|
196
|
+
* ownership of *how* it is cached - {@link listServingEndpoints} and its
|
|
197
|
+
* `CacheManager`, a plain process-lifetime field in a CLI, or a test double.
|
|
198
|
+
* Only one reload is attempted: a genuinely unknown name should fail fast
|
|
199
|
+
* rather than re-list on every request.
|
|
200
|
+
*
|
|
201
|
+
* @param load - Returns the catalogue; `force` asks it to bypass its cache.
|
|
202
|
+
*/
|
|
203
|
+
export async function rankModelIdLive(
|
|
204
|
+
load: (force: boolean) => Promise<readonly ServingEndpointSummary[]>,
|
|
205
|
+
search: string,
|
|
206
|
+
options: ResolveModelOptions = {},
|
|
207
|
+
): Promise<ResolvedModel> {
|
|
208
|
+
const resolved = rankModelId(await load(false), search, options);
|
|
209
|
+
if (resolved.matched) return resolved;
|
|
210
|
+
return rankModelId(await load(true), search, options);
|
|
211
|
+
}
|
|
212
|
+
|
|
157
213
|
/**
|
|
158
214
|
* Rank a workspace's catalogue in one call: list its `/serving-endpoints`
|
|
159
215
|
* (cached) and run {@link rankModels} over the result. The list counterpart to
|
package/src/serving.ts
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* @module
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { CacheManager } from "@databricks/appkit";
|
|
25
|
+
import { appkit } from "@dbx-tools/appkit";
|
|
24
26
|
import { error, log, string } from "@dbx-tools/shared-core";
|
|
25
27
|
import {
|
|
26
28
|
classify,
|
|
@@ -29,8 +31,6 @@ import {
|
|
|
29
31
|
type ModelProfile,
|
|
30
32
|
type ServingEndpointSummary,
|
|
31
33
|
} from "@dbx-tools/shared-model";
|
|
32
|
-
import { appkit } from "@dbx-tools/appkit";
|
|
33
|
-
import { CacheManager } from "@databricks/appkit";
|
|
34
34
|
import Fuse from "fuse.js";
|
|
35
35
|
|
|
36
36
|
import { MODEL_CLASS_ORDER } from "./classes";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
|
|
2
|
+
{
|
|
3
|
+
"extends": "../tsconfig.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"noEmit": true,
|
|
6
|
+
"rootDir": ".."
|
|
7
|
+
},
|
|
8
|
+
"include": [
|
|
9
|
+
"**/*.ts"
|
|
10
|
+
],
|
|
11
|
+
"exclude": [
|
|
12
|
+
"node_modules"
|
|
13
|
+
]
|
|
14
|
+
}
|