@dbx-tools/model 0.3.20 → 0.3.22

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 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,9 +192,11 @@ 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
 
163
200
  The AppKit-Mastra integration uses this package through
164
201
  [`@dbx-tools/appkit-mastra`](../appkit-mastra); the local OpenAI-compatible
165
- gateway uses it through [`@dbx-tools/model-proxy`](../../cli/model-proxy).
202
+ gateway uses it through [`@dbx-tools/cli-model-proxy`](../../cli/model-proxy).
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/appkit": "0.3.20",
17
- "@dbx-tools/shared-core": "0.3.20",
18
- "@dbx-tools/shared-model": "0.3.20"
16
+ "@dbx-tools/shared-core": "0.3.22",
17
+ "@dbx-tools/shared-model": "0.3.22",
18
+ "@dbx-tools/appkit": "0.3.22"
19
19
  },
20
20
  "main": "index.ts",
21
21
  "license": "UNLICENSED",
22
22
  "publishConfig": {
23
23
  "access": "public"
24
24
  },
25
- "version": "0.3.20",
25
+ "version": "0.3.22",
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 { listServingEndpoints, searchServingEndpoints, type WorkspaceClientLike } from "./serving";
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