@dbx-tools/model 0.9.40 → 0.9.46

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/package.json CHANGED
@@ -27,11 +27,11 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@databricks/appkit": "0.81.0",
30
- "@dbx-tools/appkit": "^0.9.40",
31
- "@dbx-tools/auth": "^0.9.40",
32
- "@dbx-tools/shared-auth": "^0.9.40",
33
- "@dbx-tools/shared-core": "^0.9.40",
34
- "@dbx-tools/shared-model": "^0.9.40",
30
+ "@dbx-tools/appkit": "^0.9.46",
31
+ "@dbx-tools/auth": "^0.9.46",
32
+ "@dbx-tools/shared-auth": "^0.9.46",
33
+ "@dbx-tools/shared-core": "^0.9.46",
34
+ "@dbx-tools/shared-model": "^0.9.46",
35
35
  "cacache": "^21.0.1",
36
36
  "env-paths": "^4.0.0",
37
37
  "fuse.js": "^7.4.2"
@@ -47,6 +47,10 @@
47
47
  "types": "./lib/index.d.ts",
48
48
  "default": "./lib/index.js"
49
49
  },
50
+ "./bindings": {
51
+ "types": "./lib/src/bindings.d.ts",
52
+ "default": "./lib/src/bindings.js"
53
+ },
50
54
  "./classes": {
51
55
  "types": "./lib/src/classes.d.ts",
52
56
  "default": "./lib/src/classes.js"
@@ -98,7 +102,7 @@
98
102
  "./package.json": "./package.json"
99
103
  }
100
104
  },
101
- "version": "0.9.40",
105
+ "version": "0.9.46",
102
106
  "types": "./lib/index.d.ts",
103
107
  "dbxToolsConfig": {
104
108
  "tags": [
@@ -111,6 +115,10 @@
111
115
  "types": "./lib/index.d.ts",
112
116
  "default": "./lib/index.js"
113
117
  },
118
+ "./bindings": {
119
+ "types": "./lib/src/bindings.d.ts",
120
+ "default": "./lib/src/bindings.js"
121
+ },
114
122
  "./classes": {
115
123
  "types": "./lib/src/classes.d.ts",
116
124
  "default": "./lib/src/classes.js"
@@ -7,7 +7,7 @@ import type {
7
7
  ReasoningModelsSnapshot,
8
8
  RetiredModelsSnapshot,
9
9
  } from "./_metadata-contract.ts";
10
- import { modelSearchQuery } from "./policy.ts";
10
+ import { inheritsNativeWebSearch, modelSearchQuery } from "./policy.ts";
11
11
  import {
12
12
  parseReasoning,
13
13
  REASONING_LEVELS,
@@ -132,10 +132,7 @@ export function parseModelRateLimits(html: string, generatedAt: number): ModelRa
132
132
  }
133
133
 
134
134
  /** Parse reasoning-effort ladders from the Query reasoning models HTML table. */
135
- export function parseReasoningModels(
136
- html: string,
137
- generatedAt: number,
138
- ): ReasoningModelsSnapshot {
135
+ export function parseReasoningModels(html: string, generatedAt: number): ReasoningModelsSnapshot {
139
136
  const $ = load(html);
140
137
  const models: Record<string, ReasoningLevel[]> = {};
141
138
  $("table").each((_, table) => {
@@ -147,7 +144,10 @@ export function parseReasoningModels(
147
144
  if (!headers.some((header) => header.includes("model"))) return;
148
145
  if (!headers.some((header) => header.includes("parameter"))) return;
149
146
  rows.slice(1).each((__, row) => {
150
- const cells = $(row).find("th, td").toArray().map((cell) => $(cell));
147
+ const cells = $(row)
148
+ .find("th, td")
149
+ .toArray()
150
+ .map((cell) => $(cell));
151
151
  if (cells.length < 2) return;
152
152
  const modelNames = reasoningModelNamesFromCell(cells[0]!, $);
153
153
  if (modelNames.length === 0) return;
@@ -268,7 +268,6 @@ function modelsAfterHeading($: CheerioAPI, headingId: string): string[] {
268
268
  }
269
269
 
270
270
  function nativeWebSearchModels($: CheerioAPI, responses: readonly string[]): string[] {
271
- const families = new Set<string>();
272
271
  const models = new Set<string>();
273
272
  let nativeSection = false;
274
273
  for (const element of sectionElements($, "supported-models")) {
@@ -285,11 +284,10 @@ function nativeWebSearchModels($: CheerioAPI, responses: readonly string[]): str
285
284
  const model = modelKey(value);
286
285
  if (!model) continue;
287
286
  models.add(model);
288
- families.add(model.split("-", 1)[0]);
289
287
  }
290
288
  }
291
289
  for (const model of responses) {
292
- if (families.has(model.split("-", 1)[0])) models.add(model);
290
+ if (inheritsNativeWebSearch(model, models)) models.add(model);
293
291
  }
294
292
  return [...models].sort();
295
293
  }
@@ -307,11 +305,13 @@ function codeValuesAfterHeading($: CheerioAPI, headingId: string): string[] {
307
305
 
308
306
  function sectionElements($: CheerioAPI, headingId: string): Selection[] {
309
307
  const heading = $(`#${headingId}`).first();
310
- if (heading.length === 0)
308
+ if (heading.length === 0) {
311
309
  throw new Error(`Databricks capability page is missing section ${headingId}`);
310
+ }
312
311
  const level = headingLevel(nodeTagName(heading.get(0)));
313
- if (level === undefined)
312
+ if (level === undefined) {
314
313
  throw new Error(`Databricks capability section ${headingId} is not a heading`);
314
+ }
315
315
  const elements: Selection[] = [];
316
316
  let sibling = heading.next();
317
317
  while (sibling.length > 0) {
package/src/_ranking.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  import { toModelDisplayName } from "@dbx-tools/shared-model/display";
9
9
  import Fuse from "fuse.js";
10
10
 
11
+ import { modelStatusFor } from "./_retirement.ts";
11
12
  import { classesAtOrBelow, CHAT_CLASS_ORDER, MODEL_CLASS_ORDER } from "./classes.ts";
12
13
  import {
13
14
  classifyEndpoints,
@@ -15,7 +16,6 @@ import {
15
16
  supportsToolsByFamily,
16
17
  versionTuple,
17
18
  } from "./classify.ts";
18
- import { modelStatusFor } from "./metadata.ts";
19
19
  import { modelFamily, modelReasoningEfforts, modelServiceNames } from "./policy.ts";
20
20
 
21
21
  type ModelClass = ModelClassType;
@@ -34,8 +34,9 @@ export function rankEndpoints(
34
34
  query: ModelQuery = {},
35
35
  options: NativeRankingOptions = {},
36
36
  ): RankedModel[] {
37
- const originalByName = new Map(endpoints.map((endpoint) => [endpoint.name, endpoint]));
38
- const normalized = endpoints.map((endpoint) => ({
37
+ const filtered = endpoints.filter((endpoint) => matchesModelQuery(endpoint, query));
38
+ const originalByName = new Map(filtered.map((endpoint) => [endpoint.name, endpoint]));
39
+ const normalized = filtered.map((endpoint) => ({
39
40
  ...endpoint,
40
41
  ...(options.modelClass ? { class: options.modelClass } : {}),
41
42
  ...(options.task ? { task: options.task } : {}),
@@ -50,13 +51,14 @@ export function rankEndpoints(
50
51
  }
51
52
  : classifyEndpoints(normalized);
52
53
  const requestedClass = options.modelClass ?? query.modelClass;
54
+ const includeDeprecated = query.includeDeprecated ?? options.includeDeprecated ?? false;
53
55
  const eligible = requestedClass ? classesAtOrBelow(requestedClass) : CHAT_CLASS_ORDER;
54
56
  const candidates: RankedModel[] = [];
55
57
  const search = query.search?.trim();
56
58
  if (search && requestedClass === undefined) {
57
59
  const classByName = classifyEndpointClasses(normalized);
58
60
  for (const endpoint of normalized) {
59
- if (!options.includeDeprecated && endpoint.status?.deprecated) continue;
61
+ if (!includeDeprecated && endpoint.status?.deprecated) continue;
60
62
  const capabilities = endpointCapabilities(endpoint);
61
63
  if (!capabilities.chat || (query.requiresTools && !capabilities.tools)) continue;
62
64
  candidates.push({
@@ -67,7 +69,7 @@ export function rankEndpoints(
67
69
  } else {
68
70
  for (const modelClass of eligible) {
69
71
  for (const endpoint of classified[modelClass]) {
70
- if (!options.includeDeprecated && endpoint.status?.deprecated) continue;
72
+ if (!includeDeprecated && endpoint.status?.deprecated) continue;
71
73
  if (query.requiresTools && !endpointCapabilities(endpoint).tools) continue;
72
74
  candidates.push({ endpoint, modelClass });
73
75
  }
@@ -125,6 +127,28 @@ export function rankEndpoints(
125
127
  return query.limit === undefined ? restored : restored.slice(0, Math.max(0, query.limit));
126
128
  }
127
129
 
130
+ function matchesModelQuery(endpoint: ServingEndpointSummary, query: ModelQuery): boolean {
131
+ if (query.name !== undefined && endpoint.name !== query.name) return false;
132
+ if (query.task !== undefined && endpoint.task !== query.task) return false;
133
+ if (
134
+ query.reasoningEffort !== undefined &&
135
+ !endpoint.reasoningEfforts?.includes(query.reasoningEffort)
136
+ ) {
137
+ return false;
138
+ }
139
+ if (
140
+ query.dimension !== undefined ||
141
+ query.minDimension !== undefined ||
142
+ query.maxDimension !== undefined
143
+ ) {
144
+ if (endpoint.dimension === undefined) return false;
145
+ if (query.dimension !== undefined && endpoint.dimension !== query.dimension) return false;
146
+ if (query.minDimension !== undefined && endpoint.dimension < query.minDimension) return false;
147
+ if (query.maxDimension !== undefined && endpoint.dimension > query.maxDimension) return false;
148
+ }
149
+ return true;
150
+ }
151
+
128
152
  /** Classify chat and embedding endpoints through the TypeScript-owned policy. */
129
153
  export function classifyEndpointClasses(
130
154
  endpoints: readonly ServingEndpointSummary[],
@@ -0,0 +1,70 @@
1
+ /** Lightweight model retirement state shared by discovery and metadata caches. */
2
+ import type { ModelStatus, ServingEndpointSummary } from "@dbx-tools/shared-model/contracts";
3
+
4
+ import type { RetiredModelsSnapshot } from "./_metadata-contract.ts";
5
+ import retiredModelsSnapshotJson from "./generated/retired-models.json" with { type: "json" };
6
+
7
+ const snapshot = retiredModelsSnapshotJson as RetiredModelsSnapshot;
8
+ const RETIRED_MODEL_PREFIXES = new Set([
9
+ "ai",
10
+ "anthropic",
11
+ "databricks",
12
+ "dbx",
13
+ "google",
14
+ "meta",
15
+ "openai",
16
+ "system",
17
+ ]);
18
+
19
+ let names: readonly string[] = Object.freeze([...snapshot.models]);
20
+ let keys = retiredKeys(names);
21
+
22
+ export const COMMITTED_RETIRED_MODELS = snapshot.models;
23
+ export const RETIRED_MODELS_GENERATED_AT = snapshot.generatedAt;
24
+
25
+ /** Replace retirement state after metadata cache hydration or refresh. */
26
+ export function replaceRetiredModelNames(models: readonly string[]): void {
27
+ names = Object.freeze([...models]);
28
+ keys = retiredKeys(names);
29
+ }
30
+
31
+ /** Return the active retired-model names. */
32
+ export function retiredModelNames(): readonly string[] {
33
+ return names;
34
+ }
35
+
36
+ /** Resolve whether any supplied identity is listed as retired. */
37
+ export function modelStatusFor(
38
+ ...models: readonly (string | ServingEndpointSummary)[]
39
+ ): ModelStatus {
40
+ const deprecated = models
41
+ .flatMap((model) => modelIdentities(model))
42
+ .some((identity) => {
43
+ const candidate = retiredModelKey(identity);
44
+ for (const key of keys) {
45
+ if (candidate === key || candidate.startsWith(`${key}-`)) return true;
46
+ }
47
+ return false;
48
+ });
49
+ return { deprecated };
50
+ }
51
+
52
+ function retiredKeys(models: readonly string[]): ReadonlySet<string> {
53
+ return new Set(models.map(retiredModelKey).filter(Boolean));
54
+ }
55
+
56
+ function modelIdentities(model: string | ServingEndpointSummary): string[] {
57
+ if (typeof model === "string") return [model];
58
+ return [
59
+ model.name,
60
+ model.displayName,
61
+ model.modelServiceName,
62
+ ...Object.values(model.serviceNames ?? {}),
63
+ ].filter((value): value is string => Boolean(value));
64
+ }
65
+
66
+ function retiredModelKey(value: string): string {
67
+ const tokens = value.toLowerCase().match(/[a-z0-9]+/g) ?? [];
68
+ while (tokens[0] && RETIRED_MODEL_PREFIXES.has(tokens[0])) tokens.shift();
69
+ return tokens.join("-");
70
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Binding-safe facade for Databricks model discovery and routing.
3
+ *
4
+ * Generated language bindings create one canonical model client so
5
+ * authentication, catalogue caching, ranking, and route selection remain
6
+ * attached to that client.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ export { createModelClient } from "./model-client.ts";
package/src/classify.ts CHANGED
@@ -111,15 +111,24 @@ export function versionTuple(name: string): [number, number, number] {
111
111
  return [nums[0] ?? 0, nums[1] ?? 0, nums[2] ?? 0];
112
112
  }
113
113
 
114
- /** Compare two version tuples so the higher version sorts first (descending). */
115
- function compareVersionDesc(a: readonly number[], b: readonly number[]): number {
116
- for (let i = 0; i < 3; i++) {
117
- const diff = (b[i] ?? 0) - (a[i] ?? 0);
114
+ /**
115
+ * Compare version tuples component-wise with missing parts as 0.
116
+ * Positive when `left` is newer.
117
+ */
118
+ export function compareVersionTuples(left: readonly number[], right: readonly number[]): number {
119
+ const length = Math.max(left.length, right.length, 3);
120
+ for (let index = 0; index < length; index += 1) {
121
+ const diff = (left[index] ?? 0) - (right[index] ?? 0);
118
122
  if (diff !== 0) return diff;
119
123
  }
120
124
  return 0;
121
125
  }
122
126
 
127
+ /** Compare two version tuples so the higher version sorts first (descending). */
128
+ function compareVersionDesc(a: readonly number[], b: readonly number[]): number {
129
+ return compareVersionTuples(b, a);
130
+ }
131
+
123
132
  /**
124
133
  * Monotonic version key for ordering siblings within a family/tier
125
134
  * (e.g. `opus-4-8` over `opus-4-7`). Encodes the {@link versionTuple}
@@ -21,6 +21,7 @@ import * as log from "@dbx-tools/shared-core/log";
21
21
  import cacache from "cacache";
22
22
  import envPaths from "env-paths";
23
23
 
24
+ import packageManifest from "../package.json" with { type: "json" };
24
25
  import { MODEL_METADATA_TTL_MS } from "./_metadata-contract.ts";
25
26
 
26
27
  const logger = log.logger("model/metadata-cache");
@@ -30,15 +31,10 @@ interface PackageIdentity {
30
31
  readonly version: string;
31
32
  }
32
33
 
33
- let packageIdentityPromise: Promise<PackageIdentity> | undefined;
34
-
35
- function packageIdentity(): Promise<PackageIdentity> {
36
- packageIdentityPromise ??= import("../index.ts").then((mod) => ({
37
- identifier: mod.PACKAGE_IDENTIFIER,
38
- version: mod.PACKAGE_VERSION,
39
- }));
40
- return packageIdentityPromise;
41
- }
34
+ const PACKAGE_IDENTITY: PackageIdentity = {
35
+ identifier: packageManifest.name,
36
+ version: packageManifest.version,
37
+ };
42
38
 
43
39
  /** Envelope stored as JSON bytes in cacache for every key. */
44
40
  export interface MetadataCacheRecord<T> {
@@ -247,7 +243,7 @@ async function resolveCacheDir(options: {
247
243
  readonly version?: string;
248
244
  }): Promise<string> {
249
245
  if (options.cacheDir) return options.cacheDir;
250
- const version = options.version ?? (await packageIdentity()).version;
246
+ const version = options.version ?? PACKAGE_IDENTITY.version;
251
247
  return defaultMetadataCacheDir(version);
252
248
  }
253
249
 
@@ -319,4 +315,3 @@ function isCacheMiss(error: unknown): boolean {
319
315
  (error as { code?: string }).code === "ENOENT",
320
316
  );
321
317
  }
322
-
package/src/metadata.ts CHANGED
@@ -11,12 +11,10 @@
11
11
  * @module
12
12
  */
13
13
  import * as functionUtils from "@dbx-tools/shared-core/function-utils";
14
- import * as log from "@dbx-tools/shared-core/log";
15
14
  import type {
16
15
  ModelCapabilities,
17
16
  ModelMetadata,
18
17
  ModelRateLimits,
19
- ModelStatus,
20
18
  ServingEndpointSummary,
21
19
  } from "@dbx-tools/shared-model/contracts";
22
20
 
@@ -31,7 +29,6 @@ import {
31
29
  type ModelCapabilityCatalogue,
32
30
  type ModelRateLimitsSnapshot,
33
31
  type ReasoningModelsSnapshot,
34
- type RetiredModelsSnapshot,
35
32
  } from "./_metadata-contract.ts";
36
33
  import {
37
34
  parseModelCapabilities,
@@ -39,17 +36,22 @@ import {
39
36
  parseReasoningModels,
40
37
  parseRetiredModels,
41
38
  } from "./_metadata-generator.ts";
39
+ import {
40
+ COMMITTED_RETIRED_MODELS,
41
+ modelStatusFor,
42
+ replaceRetiredModelNames,
43
+ RETIRED_MODELS_GENERATED_AT,
44
+ } from "./_retirement.ts";
45
+ import capabilitiesSnapshotJson from "./generated/model-capabilities.json" with { type: "json" };
46
+ import rateLimitsSnapshotJson from "./generated/model-rate-limits.json" with { type: "json" };
47
+ import reasoningSnapshotJson from "./generated/model-reasoning.json" with { type: "json" };
42
48
  import {
43
49
  createMetadataCache,
44
50
  mergePreferFreshList,
45
51
  mergePreferFreshRecord,
46
52
  type MetadataCache,
47
53
  } from "./metadata-cache.ts";
48
- import capabilitiesSnapshotJson from "./generated/model-capabilities.json" with { type: "json" };
49
- import rateLimitsSnapshotJson from "./generated/model-rate-limits.json" with { type: "json" };
50
- import reasoningSnapshotJson from "./generated/model-reasoning.json" with { type: "json" };
51
- import retiredModelsSnapshotJson from "./generated/retired-models.json" with { type: "json" };
52
- import { modelSearchQuery } from "./policy.ts";
54
+ import { inheritsNativeWebSearch, isFoundationModelIdentity, modelSearchQuery } from "./policy.ts";
53
55
  import {
54
56
  defaultReasoningLevels,
55
57
  documentedReasoningLevels,
@@ -69,16 +71,11 @@ export {
69
71
  WEB_SEARCH_MODELS_URL,
70
72
  };
71
73
 
72
- const logger = log.logger("model/metadata");
73
-
74
- const retiredModelsSnapshot = retiredModelsSnapshotJson as RetiredModelsSnapshot;
75
74
  const capabilitiesSnapshot = capabilitiesSnapshotJson as ModelCapabilitiesSnapshot;
76
75
  const rateLimitsSnapshot = rateLimitsSnapshotJson as ModelRateLimitsSnapshot;
77
76
  const reasoningSnapshot = reasoningSnapshotJson as ReasoningModelsSnapshot;
78
77
 
79
78
  interface MetadataIndex {
80
- readonly retiredNames: readonly string[];
81
- readonly retiredKeys: ReadonlySet<string>;
82
79
  readonly capabilities: {
83
80
  readonly [Key in keyof ModelCapabilityCatalogue]: ReadonlySet<string>;
84
81
  };
@@ -93,7 +90,7 @@ const USER_AGENT = "dbx-tools-model-metadata/1";
93
90
 
94
91
  const retiredCache = createMetadataCache<readonly string[]>({
95
92
  key: "retired-models",
96
- fallback: retiredModelsSnapshot.models,
93
+ fallback: COMMITTED_RETIRED_MODELS,
97
94
  load: async () => parseRetiredModels(await loadPage(RETIRED_MODELS_URL), nowSeconds()).models,
98
95
  merge: mergePreferFreshList,
99
96
  });
@@ -148,7 +145,9 @@ const reasoningDocsCache = createMetadataCache<Readonly<Record<string, readonly
148
145
  merge: mergePreferFreshRecord,
149
146
  });
150
147
 
151
- const learnedReasoningCache = createMetadataCache<Readonly<Record<string, readonly ReasoningLevel[]>>>({
148
+ const learnedReasoningCache = createMetadataCache<
149
+ Readonly<Record<string, readonly ReasoningLevel[]>>
150
+ >({
152
151
  key: "learned-reasoning",
153
152
  fallback: {},
154
153
  merge: mergePreferFreshRecord,
@@ -156,11 +155,13 @@ const learnedReasoningCache = createMetadataCache<Readonly<Record<string, readon
156
155
 
157
156
  let liveIndex: MetadataIndex | undefined;
158
157
 
159
- const metadataIndex = functionUtils.memoize<MetadataIndex>(() => liveIndex ?? buildIndexFromCommitted());
158
+ const metadataIndex = functionUtils.memoize<MetadataIndex>(
159
+ () => liveIndex ?? buildIndexFromCommitted(),
160
+ );
160
161
 
161
162
  function buildIndexFromCommitted(): MetadataIndex {
162
163
  return buildIndex({
163
- retiredNames: retiredModelsSnapshot.models,
164
+ retiredNames: COMMITTED_RETIRED_MODELS,
164
165
  capabilities: capabilitiesSnapshot.capabilities,
165
166
  rateLimits: rateLimitsSnapshot.catalogue.models,
166
167
  reasoning: reasoningSnapshot.catalogue.models,
@@ -175,9 +176,8 @@ function buildIndex(input: {
175
176
  readonly reasoning: Readonly<Record<string, readonly string[]>>;
176
177
  readonly learnedReasoning: Readonly<Record<string, readonly ReasoningLevel[]>>;
177
178
  }): MetadataIndex {
179
+ replaceRetiredModelNames(input.retiredNames);
178
180
  return {
179
- retiredNames: Object.freeze([...input.retiredNames]),
180
- retiredKeys: new Set(input.retiredNames.map(retiredModelKey).filter(Boolean)),
181
181
  capabilities: {
182
182
  responses: new Set(input.capabilities.responses),
183
183
  imageInput: new Set(input.capabilities.imageInput),
@@ -228,13 +228,15 @@ let refreshInflight: Promise<MetadataIndex> | undefined;
228
228
  export async function refreshModelMetadata(): Promise<MetadataIndex> {
229
229
  if (refreshInflight) return refreshInflight;
230
230
  refreshInflight = (async () => {
231
- const [retiredNames, capabilities, rateLimits, reasoning, learnedReasoning] = await Promise.all([
232
- retiredCache.get(),
233
- capabilitiesCache.get(),
234
- rateLimitsCache.get(),
235
- reasoningDocsCache.get(),
236
- learnedReasoningCache.get(),
237
- ]);
231
+ const [retiredNames, capabilities, rateLimits, reasoning, learnedReasoning] = await Promise.all(
232
+ [
233
+ retiredCache.get(),
234
+ capabilitiesCache.get(),
235
+ rateLimitsCache.get(),
236
+ reasoningDocsCache.get(),
237
+ learnedReasoningCache.get(),
238
+ ],
239
+ );
238
240
  liveIndex = buildIndex({
239
241
  retiredNames,
240
242
  capabilities,
@@ -249,14 +251,10 @@ export async function refreshModelMetadata(): Promise<MetadataIndex> {
249
251
  return refreshInflight;
250
252
  }
251
253
 
252
- void hydrateModelMetadata().catch((error) => {
253
- logger.warn("metadata cache hydrate failed; using committed snapshots", { error });
254
- });
255
-
256
254
  /** Load cacache / memory entries without forcing a network refresh. */
257
255
  export async function hydrateModelMetadata(): Promise<MetadataIndex> {
258
256
  const [retiredNames, capabilities, rateLimits, reasoning, learnedReasoning] = await Promise.all([
259
- peekOrFallback(retiredCache, retiredModelsSnapshot.models),
257
+ peekOrFallback(retiredCache, COMMITTED_RETIRED_MODELS),
260
258
  peekOrFallback(capabilitiesCache, capabilitiesSnapshot.capabilities),
261
259
  peekOrFallback(rateLimitsCache, rateLimitsSnapshot.catalogue.models),
262
260
  peekOrFallback(reasoningDocsCache, reasoningSnapshot.catalogue.models),
@@ -283,10 +281,7 @@ export function modelMetadataCachePath(): string {
283
281
  return retiredCache.path;
284
282
  }
285
283
 
286
- /** Return the committed retired-model names. The array is cached and immutable. */
287
- export function retiredModelNames(): readonly string[] {
288
- return metadataIndex().retiredNames;
289
- }
284
+ export { modelStatusFor, retiredModelNames } from "./_retirement.ts";
290
285
 
291
286
  /** Return the committed reasoning-effort catalogue. Cached per process. */
292
287
  export function reasoningModelCatalogue(): ReasoningModelCatalogue {
@@ -338,32 +333,23 @@ export async function learnReasoningLevelsFromError(
338
333
  return rememberReasoningLevels(model, parseReasoningLevels(body));
339
334
  }
340
335
 
341
- /** Resolve whether any supplied identity is listed in the retirement snapshot. */
342
- export function modelStatusFor(
343
- ...models: readonly (string | ServingEndpointSummary)[]
344
- ): ModelStatus {
345
- const retired = metadataIndex().retiredKeys;
346
- const deprecated = models
347
- .flatMap((model) => modelIdentities(model))
348
- .some((identity) => {
349
- const candidate = retiredModelKey(identity);
350
- for (const key of retired) {
351
- if (candidate === key || candidate.startsWith(`${key}-`)) return true;
352
- }
353
- return false;
354
- });
355
- return { deprecated };
356
- }
357
-
358
336
  /** Resolve documented model capabilities from a name or endpoint summary. */
359
337
  export function modelCapabilitiesFor(model: string | ServingEndpointSummary): ModelCapabilities {
360
338
  const capabilities = metadataIndex().capabilities;
361
- const keys = modelIdentities(model, false).flatMap((identity) => modelKey(identity) ?? []);
339
+ const identities = modelIdentities(model, false);
340
+ const keys = identities.flatMap((identity) => modelKey(identity) ?? []);
362
341
  return {
363
342
  responses: keys.some((key) => capabilities.responses.has(key)),
364
343
  imageInput: keys.some((key) => capabilities.imageInput.has(key)),
365
344
  applyPatch: keys.some((key) => capabilities.applyPatch.has(key)),
366
- webSearch: keys.some((key) => capabilities.webSearch.has(key)),
345
+ // Version inheritance only applies to hosted foundation identities.
346
+ webSearch:
347
+ keys.some((key) => capabilities.webSearch.has(key)) ||
348
+ identities.some(
349
+ (identity) =>
350
+ isFoundationModelIdentity(identity) &&
351
+ inheritsNativeWebSearch(identity, capabilities.webSearch),
352
+ ),
367
353
  };
368
354
  }
369
355
 
@@ -388,9 +374,7 @@ export function modelRateLimitsFor(
388
374
  * Does not parse error bodies; callers should use {@link parseReasoningLevels}
389
375
  * and then {@link rememberReasoningLevels}.
390
376
  */
391
- export function modelReasoningLevelsFor(
392
- model: string | ServingEndpointSummary,
393
- ): ReasoningLevel[] {
377
+ export function modelReasoningLevelsFor(model: string | ServingEndpointSummary): ReasoningLevel[] {
394
378
  const index = metadataIndex();
395
379
  for (const identity of modelIdentities(model)) {
396
380
  const learned = documentedReasoningLevels(identity, index.learnedReasoning);
@@ -400,8 +384,7 @@ export function modelReasoningLevelsFor(
400
384
  const documented = documentedReasoningLevels(identity, index.reasoning);
401
385
  if (documented?.length) return documented;
402
386
  }
403
- const primary =
404
- typeof model === "string" ? model : (model.modelServiceName ?? model.name);
387
+ const primary = typeof model === "string" ? model : (model.modelServiceName ?? model.name);
405
388
  return defaultReasoningLevels(primary);
406
389
  }
407
390
 
@@ -423,7 +406,7 @@ export function modelMetadataGeneratedAt(): Readonly<{
423
406
  reasoning: number;
424
407
  }> {
425
408
  return {
426
- retiredModels: retiredModelsSnapshot.generatedAt,
409
+ retiredModels: RETIRED_MODELS_GENERATED_AT,
427
410
  capabilities: capabilitiesSnapshot.generatedAt,
428
411
  rateLimits: rateLimitsSnapshot.generatedAt,
429
412
  reasoning: reasoningSnapshot.generatedAt,
@@ -447,23 +430,6 @@ function modelKey(value: string): string | undefined {
447
430
  return modelSearchQuery(value)?.replaceAll(" ", "-");
448
431
  }
449
432
 
450
- const RETIRED_MODEL_PREFIXES = new Set([
451
- "ai",
452
- "anthropic",
453
- "databricks",
454
- "dbx",
455
- "google",
456
- "meta",
457
- "openai",
458
- "system",
459
- ]);
460
-
461
- function retiredModelKey(value: string): string {
462
- const tokens = value.toLowerCase().match(/[a-z0-9]+/g) ?? [];
463
- while (tokens[0] && RETIRED_MODEL_PREFIXES.has(tokens[0])) tokens.shift();
464
- return tokens.join("-");
465
- }
466
-
467
433
  async function loadPage(url: string): Promise<string> {
468
434
  const response = await fetch(url, {
469
435
  headers: { "user-agent": USER_AGENT },
@@ -3,16 +3,17 @@
3
3
  *
4
4
  * This is the portable entry point used by generated Python bindings. It keeps
5
5
  * authentication, endpoint discovery, caching, normalization, ranking, route
6
- * selection, and published metadata in Node while exposing one small client.
6
+ * selection, route construction, and published metadata in Node while exposing
7
+ * one small client.
7
8
  *
8
9
  * @module
9
10
  */
10
11
  import {
11
- client as authClient,
12
- profile as authProfile,
12
+ createAuthClient,
13
13
  type AuthClient,
14
14
  type DatabricksAuthDependencies,
15
- } from "@dbx-tools/auth";
15
+ } from "@dbx-tools/auth/client";
16
+ import { listProfiles as listAuthProfiles } from "@dbx-tools/auth/profile";
16
17
  import type { DatabricksProfileSummary } from "@dbx-tools/shared-auth";
17
18
  import * as log from "@dbx-tools/shared-core/log";
18
19
  import * as object from "@dbx-tools/shared-core/object";
@@ -29,7 +30,7 @@ import {
29
30
  import { normalizeEndpoints } from "./_ranking.ts";
30
31
  import { lookupModels, resolveModel } from "./_selection.ts";
31
32
  import { chatCompletionsUrl, invocationsUrl, responsesUpstreamUrl } from "./invoke.ts";
32
- import { modelMetadataFor } from "./metadata.ts";
33
+ import { hydrateModelMetadata, modelMetadataFor } from "./metadata.ts";
33
34
  import { modelServingApi } from "./policy.ts";
34
35
 
35
36
  const logger = log.logger("model/client");
@@ -49,6 +50,8 @@ export interface ModelClientOptions {
49
50
  auth?: ModelAuthOptions;
50
51
  /** In-memory endpoint catalogue TTL in milliseconds. */
51
52
  cacheTtlMs?: number;
53
+ /** Hydrate Node-only metadata caches before constructing the client. */
54
+ hydrateMetadata?: boolean;
52
55
  }
53
56
 
54
57
  /** Secret-free runtime identity and cache configuration. */
@@ -246,15 +249,16 @@ class DefaultModelClient implements ModelClient {
246
249
  /** Create an authentication-aware Databricks model client. */
247
250
  export async function createModelClient(options: ModelClientOptions = {}): Promise<ModelClient> {
248
251
  const cacheTtlMs = validateCacheTtl(options.cacheTtlMs);
252
+ if (options.hydrateMetadata ?? true) await hydrateModelMetadata();
249
253
  return new DefaultModelClient(
250
- new AuthenticatedModelClient(await authClient.createAuthClient(options.auth), globalThis.fetch),
254
+ new AuthenticatedModelClient(await createAuthClient(options.auth), globalThis.fetch),
251
255
  cacheTtlMs,
252
256
  );
253
257
  }
254
258
 
255
259
  /** List configured Databricks profiles outside the model client facade. */
256
260
  export function listProfiles(refresh = false): DatabricksProfileSummary[] {
257
- return authProfile.listProfiles({ refresh });
261
+ return listAuthProfiles({ refresh });
258
262
  }
259
263
 
260
264
  /** @internal Construct a model client around a test or host-owned Databricks client. */
@@ -270,9 +274,10 @@ export async function createModelClientWithDependencies(
270
274
  options: ModelClientOptions,
271
275
  dependencies: DatabricksAuthDependencies,
272
276
  ): Promise<ModelClient> {
277
+ if (options.hydrateMetadata ?? true) await hydrateModelMetadata();
273
278
  return new DefaultModelClient(
274
279
  new AuthenticatedModelClient(
275
- await authClient.createAuthClient(options.auth, dependencies),
280
+ await createAuthClient(options.auth, dependencies),
276
281
  dependencies.fetch ?? globalThis.fetch,
277
282
  ),
278
283
  validateCacheTtl(options.cacheTtlMs),