@dbx-tools/appkit-mastra 0.1.41 → 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/index.ts CHANGED
@@ -11,29 +11,18 @@
11
11
  * consumers.
12
12
  */
13
13
  export * from "@dbx-tools/appkit-mastra-shared";
14
+ export * from "@dbx-tools/model";
14
15
  export * from "./src/agents.js";
15
16
  export * from "./src/chart.js";
16
17
  export * from "./src/config.js";
17
18
  export * from "./src/genie.js";
18
- export {
19
- FALLBACK_MODEL_IDS,
20
- MODEL_CATALOG,
21
- ModelTier,
22
- modelForTier,
23
- modelsForTier,
24
- } from "./src/model.js";
25
19
  export * from "./src/plugin.js";
26
20
  export {
27
21
  MASTRA_MODEL_OVERRIDE_KEY,
28
22
  MODEL_OVERRIDE_BODY_FIELDS,
29
23
  MODEL_OVERRIDE_HEADER,
30
24
  MODEL_OVERRIDE_QUERY,
31
- clearServingEndpointsCache,
32
25
  extractModelOverride,
33
- listServingEndpoints,
34
- resolveModelId,
35
- type ResolveModelOptions,
36
- type ResolvedModel,
37
- type ServingEndpointSummary,
26
+ type ModelOverrideRequest,
38
27
  } from "./src/serving.js";
39
28
  export * from "./src/tools/email.js";
package/package.json CHANGED
@@ -9,13 +9,14 @@
9
9
  }
10
10
  },
11
11
  "name": "@dbx-tools/appkit-mastra",
12
- "version": "0.1.41",
12
+ "version": "0.1.42",
13
13
  "dependencies": {
14
14
  "@databricks/sdk-experimental": "^0.17",
15
- "@dbx-tools/appkit-mastra-shared": "0.1.41",
16
- "@dbx-tools/genie": "0.1.41",
17
- "@dbx-tools/genie-shared": "0.1.41",
18
- "@dbx-tools/shared": "0.1.41",
15
+ "@dbx-tools/appkit-mastra-shared": "0.1.42",
16
+ "@dbx-tools/genie": "0.1.42",
17
+ "@dbx-tools/genie-shared": "0.1.42",
18
+ "@dbx-tools/model": "0.1.42",
19
+ "@dbx-tools/shared": "0.1.42",
19
20
  "@mastra/ai-sdk": "^1",
20
21
  "@mastra/core": "^1",
21
22
  "@mastra/express": "^1",
@@ -24,7 +25,6 @@
24
25
  "@mastra/observability": "^1",
25
26
  "@mastra/otel-bridge": "^1",
26
27
  "@mastra/pg": "^1",
27
- "fuse.js": "^7.0.0",
28
28
  "pg": "^8.20.0",
29
29
  "zod": "^4.3.6"
30
30
  },
package/src/agents.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * When no agents are registered the plugin falls back to a single
12
12
  * built-in analyst so the bare `mastra()` call still mounts a working
13
- * `chatRoute` agent for demos.
13
+ * streamable agent for demos.
14
14
  */
15
15
 
16
16
  import { appkitUtils, logUtils, stringUtils } from "@dbx-tools/shared";
@@ -226,8 +226,8 @@ export type MastraToolsFn = (
226
226
  /**
227
227
  * A code-defined Mastra agent. Mirrors the shape AppKit's `agents`
228
228
  * plugin uses for `AgentDefinition`. The registry key under
229
- * `config.agents` is what `chatRoute` matches on; `name` is purely
230
- * informational (defaults to the key).
229
+ * `config.agents` is the `agentId` the client streams against; `name`
230
+ * is purely informational (defaults to the key).
231
231
  */
232
232
  export interface MastraAgentDefinition {
233
233
  /** Display name used as `Agent.name`. Defaults to the registry key. */
package/src/chart.ts CHANGED
@@ -39,7 +39,7 @@ import { createTool } from "@mastra/core/tools";
39
39
  import { z } from "zod";
40
40
 
41
41
  import type { MastraPluginConfig } from "./config.js";
42
- import { buildModel, modelForTier, ModelTier } from "./model.js";
42
+ import { buildModel, ModelTier } from "./model.js";
43
43
 
44
44
  const log = logUtils.logger("mastra/chart");
45
45
 
@@ -330,9 +330,7 @@ function getPlannerAgent(config: MastraPluginConfig): Agent {
330
330
  description: "Picks chart type and axis encodings for a dataset.",
331
331
  instructions: CHART_PLANNER_INSTRUCTIONS,
332
332
  model: ({ requestContext }) =>
333
- buildModel(config, requestContext, {
334
- modelId: modelForTier(ModelTier.Fast),
335
- }),
333
+ buildModel(config, requestContext, { tier: ModelTier.Fast }),
336
334
  });
337
335
  plannerAgents.set(config, agent);
338
336
  }
@@ -361,7 +359,7 @@ async function runChartPlanner(
361
359
  ...(requestContext ? { requestContext } : {}),
362
360
  ...(abortSignal ? { abortSignal } : {}),
363
361
  });
364
- const plan = result.object as ChartPlan;
362
+ const plan = chartPlanSchema.parse(result.object);
365
363
  const option = planToEchartsOption(plan, title);
366
364
  return { chartType: plan.chartType, option };
367
365
  }
package/src/config.ts CHANGED
@@ -150,7 +150,8 @@ export interface MastraPluginConfig extends BasePluginConfig {
150
150
  */
151
151
  tools?: MastraTools;
152
152
  /**
153
- * Agent id used by `chatRoute` when the client doesn't specify one.
153
+ * Agent id used when the client doesn't specify one (the bare,
154
+ * un-suffixed history / suggestions routes resolve to it).
154
155
  * Defaults to the first key in `agents` (or `"default"` when
155
156
  * `agents` is omitted). Must match an id in `agents` when both are
156
157
  * set; a mismatch throws at setup with the available candidates.
@@ -198,17 +199,19 @@ export interface MastraPluginConfig extends BasePluginConfig {
198
199
  */
199
200
  modelOverride?: boolean;
200
201
  /**
201
- * Priority-ordered list of endpoint names tried when no agent /
202
- * plugin / env / request-override model id is set. The resolver
203
- * picks the first id that is actually present in the workspace's
204
- * `/serving-endpoints` listing - this is what lets a workspace
205
- * without Claude Opus still get a sensible default automatically.
202
+ * Priority-ordered list of endpoint names tried *first* when no
203
+ * agent / plugin / env / request-override model id is set, ahead of
204
+ * the dynamic score-classified catalogue. The resolver picks the
205
+ * first id that is actually present in the workspace's
206
+ * `/serving-endpoints` listing.
206
207
  *
207
- * Defaults to the built-in list in `model.ts` (`FALLBACK_MODEL_IDS`):
208
- * Claude (Opus -> Sonnet -> Haiku), then OpenAI GPT-5 family, then
209
- * open weights (Llama 4, Llama 3.3, GPT-OSS, Qwen, Llama 3.1).
210
- * Override here to pin a regulated workspace to an approved subset
211
- * or to add custom endpoints in front of the public catalogue.
208
+ * When unset, resolution is driven by the live Foundation Model API
209
+ * `quality` / `speed` / `cost` scores: endpoints are classified into
210
+ * tiers (`classifyEndpoints`) and walked best-first (Thinking ->
211
+ * Balanced -> Fast), with the small built-in `FALLBACK_MODEL_IDS`
212
+ * list as the floor when the catalogue can't be read. Set this to
213
+ * pin a regulated workspace to an approved subset, or to put custom
214
+ * endpoints in front of the auto-classified catalogue.
212
215
  */
213
216
  defaultModelFallbacks?: readonly string[];
214
217
  /**
package/src/genie.ts CHANGED
@@ -38,11 +38,11 @@ import { CacheManager, genie } from "@databricks/appkit";
38
38
  import { ApiError, HttpError, WorkspaceClient } from "@databricks/sdk-experimental";
39
39
  import {
40
40
  ChartSchema,
41
- type MinimalWriter,
41
+ type MastraWriter,
42
42
  type StartedEvent,
43
43
  } from "@dbx-tools/appkit-mastra-shared";
44
44
  import { genieEventChat, genieSampleQuestions, getGenieSpace } from "@dbx-tools/genie";
45
- import { type GenieMessage } from "@dbx-tools/genie-shared";
45
+ import { GenieMessageSchema, type GenieMessage } from "@dbx-tools/genie-shared";
46
46
  import { appkitUtils, commonUtils, logUtils, stringUtils } from "@dbx-tools/shared";
47
47
  import type { RequestContext } from "@mastra/core/request-context";
48
48
  import { MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
@@ -93,7 +93,7 @@ export type GenieSpacesConfig = Record<string, GenieSpaceConfig | string>;
93
93
  type ToolExecuteCtx =
94
94
  | {
95
95
  requestContext?: RequestContext;
96
- writer?: MinimalWriter;
96
+ writer?: MastraWriter;
97
97
  abortSignal?: AbortSignal;
98
98
  }
99
99
  | undefined;
@@ -421,7 +421,7 @@ function buildAskGenieTool(opts: { spaceId: string; alias: string; hint?: string
421
421
  question: z.string().min(1, "question is required"),
422
422
  }),
423
423
  outputSchema: z.object({
424
- message: z.custom<GenieMessage>(),
424
+ message: GenieMessageSchema,
425
425
  }),
426
426
  execute: async ({ question }, ctxRaw) => {
427
427
  const ctx = ctxRaw as ToolExecuteCtx;
@@ -1033,24 +1033,6 @@ export function buildGenieToolkitProvider(opts: {
1033
1033
  };
1034
1034
  }
1035
1035
 
1036
- /**
1037
- * Returns `true` when at least one Genie space is reachable
1038
- * through {@link resolveGenieSpaces} - either via
1039
- * {@link MastraPluginConfig.genieSpaces}, the AppKit `genie()`
1040
- * plugin instance, or the `DATABRICKS_GENIE_SPACE_ID` env var.
1041
- *
1042
- * Cheap to call from `resolveProvider` to short-circuit `genie`
1043
- * lookups when nothing is wired, so the `plugins.genie` lookup
1044
- * still resolves to `undefined` (matching AppKit's
1045
- * absent-plugin semantics) when neither source is configured.
1046
- */
1047
- export function hasAnyGenieSpaces(
1048
- config: MastraPluginConfig,
1049
- context: appkitUtils.PluginContextLike | undefined,
1050
- ): boolean {
1051
- return Object.keys(resolveGenieSpaces(config, context)).length > 0;
1052
- }
1053
-
1054
1036
  /* --------------------------- starter suggestions --------------------------- */
1055
1037
 
1056
1038
  /**
package/src/history.ts CHANGED
@@ -9,8 +9,8 @@
9
9
  *
10
10
  * The route is registered through {@link historyRoute} as a Mastra
11
11
  * `registerApiRoute` so it sits in the same dispatcher pipeline as
12
- * `chatRoute`. That means the `MastraServer` auth middleware (in
13
- * `./server.ts`) has already populated `RequestContext` with
12
+ * the standard agent routes. That means the `MastraServer` auth
13
+ * middleware (in `./server.ts`) has already populated `RequestContext` with
14
14
  * `MASTRA_THREAD_ID_KEY` and `MASTRA_RESOURCE_ID_KEY` by the time
15
15
  * the handler runs - no cookie or user lookups happen here, and the
16
16
  * session-cookie logic stays the single source of truth in `server.ts`.
@@ -197,10 +197,10 @@ export type HistoryRouteOptions =
197
197
  * anchors the thread id is left alone so the user keeps the
198
198
  * same thread - only the contents go away.
199
199
  *
200
- * Modeled after `chatRoute` from `@mastra/ai-sdk`: pass `agent` for a
201
- * fixed-agent mount, or include `:agentId` in the path for dynamic
202
- * routing. Pairs cleanly with the AppKit Mastra plugin's chat route
203
- * layout (`/route/chat` + `/route/chat/:agentId`).
200
+ * Follows the `@mastra/ai-sdk` `chatRoute` convention for agent
201
+ * binding: pass `agent` for a fixed-agent mount, or include
202
+ * `:agentId` in the path for dynamic routing. The plugin registers
203
+ * both `/route/history` (default agent) and `/route/history/:agentId`.
204
204
  *
205
205
  * The handler reads `threadId` and `resourceId` from `RequestContext`
206
206
  * (populated upstream by `MastraServer.registerAuthMiddleware`), so
package/src/model.ts CHANGED
@@ -7,237 +7,53 @@
7
7
  * fresh bearer header, then point Mastra's OpenAI-compatible provider
8
8
  * at `/serving-endpoints` on that host.
9
9
  *
10
- * Model id resolution walks three sources before falling back to the
11
- * hard-coded default, **in this priority order**:
12
- *
13
- * 1. Per-request override stashed by the auth middleware under
14
- * {@link MASTRA_MODEL_OVERRIDE_KEY} (header / query / body).
15
- * 2. The static `modelId` passed in by the agent / plugin (string
16
- * sugar on `def.model` or `config.defaultModel`).
17
- * 3. `DATABRICKS_SERVING_ENDPOINT_NAME` env var.
18
- * 4. {@link FALLBACK_MODEL_ID}.
19
- *
20
- * Whatever wins is then fuzzy-matched against the live
21
- * `/serving-endpoints` list ({@link listServingEndpoints}) so loose
22
- * names like `"claude sonnet"` resolve to the real endpoint name.
23
- * Fuzzy matching is best-effort: when the workspace client throws
24
- * (network blip, expired token at cache-fill time) we fall back to
25
- * the input verbatim and let Databricks return the canonical error.
10
+ * This module only adds the Mastra-specific glue. The actual model
11
+ * selection - listing the workspace catalogue and resolving an
12
+ * explicit name / tier / fallback chain to a real endpoint id - lives
13
+ * in `@dbx-tools/model` ({@link selectModel}) so non-Mastra consumers
14
+ * (e.g. a job that just needs a model name) can reuse it. Here we
15
+ * assemble the explicit ask from Mastra's request context (the
16
+ * per-request override under {@link MASTRA_MODEL_OVERRIDE_KEY}, the
17
+ * agent / plugin `modelId`, or `DATABRICKS_SERVING_ENDPOINT_NAME`),
18
+ * pass the plugin's fuzzy / tier / fallback knobs through, and wrap
19
+ * the resolved id in the OpenAI-compatible provider config Mastra
20
+ * expects. Catalogue fetches fail loud: network / auth errors
21
+ * propagate so callers see the real SDK message.
26
22
  */
27
23
 
24
+ import { type ModelTier, parseModelTier, selectModel } from "@dbx-tools/model";
28
25
  import { commonUtils, logUtils, netUtils, stringUtils } from "@dbx-tools/shared";
29
26
  import type { MastraModelConfig } from "@mastra/core/llm";
30
27
  import type { RequestContext } from "@mastra/core/request-context";
31
28
 
32
29
  import { MASTRA_USER_KEY, type MastraPluginConfig, type User } from "./config.js";
33
- import {
34
- listServingEndpoints,
35
- MASTRA_MODEL_OVERRIDE_KEY,
36
- resolveModelId,
37
- resolveServingConfig,
38
- type ServingEndpointSummary,
39
- } from "./serving.js";
40
-
41
- /**
42
- * Capability tiers for Databricks Foundation Model API endpoints.
43
- *
44
- * - {@link ModelTier.Thinking}: deepest reasoning / "thinking" models
45
- * (Claude Opus, GPT-5.5 Pro, Gemini Pro, Llama 4 Maverick, etc).
46
- * Highest cost and latency; reserve for hard multi-step reasoning.
47
- * - {@link ModelTier.Balanced}: cost/latency sweet spot for general
48
- * agent work (Claude Sonnet, GPT-5.x, Gemini Flash, Llama 3.3 70B).
49
- * The right default for most agents.
50
- * - {@link ModelTier.Fast}: cheap and quick; classification, routing,
51
- * tool-arg extraction, simple summarisation (Claude Haiku, GPT-5
52
- * mini/nano, Gemini Flash Lite, GPT-OSS 20B, Llama 3.1 8B).
53
- *
54
- * String enum so the value is the slug we use in cache keys, logs,
55
- * and as the value users see in serialized configs.
56
- */
57
- export enum ModelTier {
58
- Thinking = "thinking",
59
- Balanced = "balanced",
60
- Fast = "fast",
61
- }
62
-
63
- /**
64
- * Catalogue of Databricks-hosted Foundation Model API endpoints,
65
- * grouped by capability {@link ModelTier} and then by provider. Each
66
- * inner array is priority-ordered (most powerful first within the
67
- * same provider+tier).
68
- *
69
- * Provider buckets:
70
- *
71
- * - `claude`: Anthropic Claude family (closed; flagship reasoning).
72
- * - `gpt`: OpenAI GPT-5 family (closed; "ChatGPT" on Databricks FMAPI).
73
- * - `gemini`: Google Gemini family (closed; multimodal + web-search).
74
- * - `openSource`: open-weights models (widest regional / SKU availability).
75
- *
76
- * The list is curated by hand; refresh from the Databricks "supported
77
- * foundation models" doc when new endpoints land.
78
- */
79
- export const MODEL_CATALOG = {
80
- [ModelTier.Thinking]: {
81
- claude: [
82
- "databricks-claude-opus-4-8",
83
- "databricks-claude-opus-4-7",
84
- "databricks-claude-opus-4-6",
85
- "databricks-claude-opus-4-5",
86
- "databricks-claude-opus-4-1",
87
- ],
88
- gpt: ["databricks-gpt-5-5-pro"],
89
- gemini: [
90
- "databricks-gemini-3-1-pro",
91
- "databricks-gemini-3-pro",
92
- "databricks-gemini-2-5-pro",
93
- ],
94
- openSource: [
95
- "databricks-llama-4-maverick",
96
- "databricks-gpt-oss-120b",
97
- "databricks-meta-llama-3-1-405b-instruct",
98
- ],
99
- },
100
- [ModelTier.Balanced]: {
101
- claude: [
102
- "databricks-claude-sonnet-4-6",
103
- "databricks-claude-sonnet-4-5",
104
- "databricks-claude-sonnet-4",
105
- ],
106
- gpt: [
107
- "databricks-gpt-5-5",
108
- "databricks-gpt-5-4",
109
- "databricks-gpt-5-2",
110
- "databricks-gpt-5-1",
111
- "databricks-gpt-5",
112
- ],
113
- gemini: [
114
- "databricks-gemini-3-5-flash",
115
- "databricks-gemini-3-flash",
116
- "databricks-gemini-2-5-flash",
117
- ],
118
- openSource: [
119
- "databricks-meta-llama-3-3-70b-instruct",
120
- "databricks-qwen3-next-80b-a3b-instruct",
121
- "databricks-qwen35-122b-a10b",
122
- ],
123
- },
124
- [ModelTier.Fast]: {
125
- claude: ["databricks-claude-haiku-4-5"],
126
- gpt: [
127
- "databricks-gpt-5-4-mini",
128
- "databricks-gpt-5-4-nano",
129
- "databricks-gpt-5-mini",
130
- "databricks-gpt-5-nano",
131
- ],
132
- gemini: ["databricks-gemini-3-1-flash-lite"],
133
- openSource: [
134
- "databricks-gpt-oss-20b",
135
- "databricks-gemma-3-12b",
136
- "databricks-meta-llama-3-1-8b-instruct",
137
- ],
138
- },
139
- } as const satisfies Record<ModelTier, Record<string, readonly string[]>>;
140
-
141
- /**
142
- * Round-robin zip: take one from each input list in order, skipping
143
- * lists that have already been exhausted. Used to interleave provider
144
- * buckets within a tier so the resolver alternates between vendors
145
- * instead of draining one before trying the next.
146
- *
147
- * Example: `interleave(["a1","a2","a3"], ["b1","b2"])` ->
148
- * `["a1","b1","a2","b2","a3"]`.
149
- */
150
- function interleave<T>(...lists: readonly (readonly T[])[]): T[] {
151
- const out: T[] = [];
152
- const max = Math.max(0, ...lists.map((l) => l.length));
153
- for (let i = 0; i < max; i++) {
154
- for (const list of lists) {
155
- if (i < list.length) out.push(list[i]!);
156
- }
157
- }
158
- return out;
159
- }
160
-
161
- /**
162
- * Priority-ordered model ids for a single capability {@link ModelTier},
163
- * interleaved across providers so a workspace missing the top Claude
164
- * still lands on a flagship GPT / Gemini on the next probe.
165
- *
166
- * Provider order within the interleave: Claude, GPT, Gemini, then the
167
- * open-weights tail appended verbatim as the universal floor (widest
168
- * regional availability).
169
- *
170
- * @example
171
- * ```ts
172
- * mastra({
173
- * defaultModelFallbacks: modelsForTier(ModelTier.Fast),
174
- * });
175
- * ```
176
- */
177
- export function modelsForTier(tier: ModelTier): readonly string[] {
178
- const bucket = MODEL_CATALOG[tier];
179
- return [
180
- ...interleave(bucket.claude, bucket.gpt, bucket.gemini),
181
- ...bucket.openSource,
182
- ];
183
- }
30
+ import { MASTRA_MODEL_OVERRIDE_KEY, resolveServingConfig } from "./serving.js";
184
31
 
185
- /**
186
- * Top model id at the given {@link ModelTier}. Sync; the agent-step
187
- * resolver fuzzy-matches it against the workspace catalogue at call
188
- * time, so this works even when the literal top pick isn't deployed.
189
- *
190
- * Use when wiring a tier-appropriate model into an agent definition:
191
- *
192
- * @example
193
- * ```ts
194
- * const classifier = createAgent({
195
- * instructions: "Classify this email",
196
- * model: modelForTier(ModelTier.Fast), // cheap, quick
197
- * });
198
- *
199
- * const planner = createAgent({
200
- * instructions: "Plan a multi-step migration",
201
- * model: modelForTier(ModelTier.Thinking), // deep reasoning
202
- * });
203
- * ```
204
- */
205
- export function modelForTier(tier: ModelTier): string {
206
- return modelsForTier(tier)[0]!;
207
- }
208
-
209
- /**
210
- * Last-resort model ids used when neither `config.defaultModel`,
211
- * per-agent `model`, nor `DATABRICKS_SERVING_ENDPOINT_NAME` is set.
212
- *
213
- * Walked in order at resolve time: the first id whose endpoint is
214
- * actually present in the workspace's `/serving-endpoints` listing
215
- * wins. Workspaces vary - not every region / SKU has every model,
216
- * and the list of Foundation Model APIs evolves quickly - so the
217
- * resolver degrades all the way from "best thinking model" down to
218
- * "smallest commodity Llama" before giving up.
219
- *
220
- * Built by chaining the per-tier interleaves (Thinking -> Balanced
221
- * -> Fast); within each tier the providers are round-robin-zipped
222
- * (Claude, GPT, Gemini, then open-weights tail). Override the entire
223
- * list via `MastraPluginConfig.defaultModelFallbacks` (e.g. to pin a
224
- * regulated workspace to a specific approved subset, or to bias the
225
- * priority toward a particular tier).
226
- */
227
- export const FALLBACK_MODEL_IDS: readonly string[] = [
228
- ...modelsForTier(ModelTier.Thinking),
229
- ...modelsForTier(ModelTier.Balanced),
230
- ...modelsForTier(ModelTier.Fast),
231
- ];
32
+ export {
33
+ classifyEndpoints,
34
+ FALLBACK_MODEL_IDS,
35
+ modelForTier,
36
+ modelsForTier,
37
+ ModelTier,
38
+ } from "@dbx-tools/model";
232
39
 
233
40
  /** Optional overrides accepted by {@link buildModel}. */
234
41
  export interface BuildModelOverrides {
235
42
  /**
236
43
  * Static model id from the agent / plugin config (string sugar on
237
44
  * `def.model` or `config.defaultModel`). Loses to the per-request
238
- * override but wins over env / fallback.
45
+ * override but wins over env / tier / fallback.
239
46
  */
240
47
  modelId?: string;
48
+ /**
49
+ * Capability tier to resolve when no explicit model id is supplied.
50
+ * Used by internal agents (e.g. the chart planner asks for
51
+ * {@link ModelTier.Fast}) to express intent without pinning an
52
+ * endpoint name; the live catalogue is classified and the top
53
+ * available model in the tier is chosen, falling back to the
54
+ * tier's static list when the workspace has none.
55
+ */
56
+ tier?: ModelTier;
241
57
  }
242
58
 
243
59
  /**
@@ -262,91 +78,41 @@ export async function buildModel(
262
78
  // well-formed (`/serving-endpoints/chat/completions`).
263
79
  const url = new URL("/serving-endpoints", host).toString().replace(/\/$/, "");
264
80
 
265
- const modelId = await pickModelId(config, requestContext, overrides, user, host);
266
-
267
- return {
268
- providerId: config.providerId ?? "databricks",
269
- modelId,
270
- url,
271
- headers: Object.fromEntries(headers.entries()),
272
- };
273
- }
274
-
275
- /**
276
- * Walk the resolution ladder and pick a modelId.
277
- *
278
- * 1. **Explicit ask** (per-request override, agent `model` string,
279
- * `config.defaultModel` string, or `DATABRICKS_SERVING_ENDPOINT_NAME`):
280
- * when fuzzy matching is on, snap the input to the closest live
281
- * endpoint so loose names like `"claude sonnet"` resolve. When it's
282
- * off (or no endpoint matches within threshold), the input is used
283
- * verbatim and Databricks surfaces the canonical 404.
284
- *
285
- * 2. **No explicit ask**: walk
286
- * {@link MastraPluginConfig.defaultModelFallbacks} (or
287
- * {@link FALLBACK_MODEL_IDS} when unset) and return the first id
288
- * whose endpoint is actually present in the workspace listing. A
289
- * workspace without Claude Opus still gets a sensible default by
290
- * skipping ahead to whichever Sonnet / GPT-5 / Llama variant is
291
- * wired up.
292
- *
293
- * Catalogue fetches fail loud: network / auth errors propagate to the
294
- * caller so they see the real SDK message instead of a silent fallback
295
- * to the top of the priority list.
296
- */
297
- async function pickModelId(
298
- config: MastraPluginConfig,
299
- requestContext: RequestContext,
300
- overrides: BuildModelOverrides,
301
- user: User,
302
- host: string,
303
- ): Promise<string> {
304
81
  const log = logUtils.logger(config);
305
- const serving = resolveServingConfig(config, FALLBACK_MODEL_IDS);
82
+ const serving = resolveServingConfig(config);
306
83
  const override = serving.allowOverride
307
84
  ? (requestContext.get(MASTRA_MODEL_OVERRIDE_KEY) as string | undefined)
308
85
  : undefined;
309
- const explicit =
310
- override ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
311
86
 
312
- // Cheap exit: when the caller named a specific model and fuzzy
313
- // matching is off, there's no reason to touch the catalogue at all.
314
- if (explicit !== undefined && !serving.fuzzy) {
315
- log.debug("model selected", { modelId: explicit, source: "explicit" });
316
- return explicit;
317
- }
87
+ // The override / agent default / env value can be either a concrete
88
+ // endpoint name or a capability tier slug ("thinking" / "balanced" /
89
+ // "fast"). A tier slug becomes a tier intent (let the live catalogue
90
+ // pick the best model in that band); anything else is an explicit
91
+ // name fuzzy-matched against the catalogue. An internal
92
+ // `overrides.tier` (e.g. the chart planner) is the floor when nothing
93
+ // was requested.
94
+ const requested =
95
+ override ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
96
+ const requestedTier = requested !== undefined ? parseModelTier(requested) : null;
97
+ const explicit = requestedTier === null ? requested : undefined;
98
+ const tier = requestedTier ?? overrides.tier;
318
99
 
319
- const endpoints = await listServingEndpoints(user.executionContext.client, host, {
100
+ const { modelId, source } = await selectModel(user.executionContext.client, host, {
101
+ ...(explicit !== undefined ? { explicit } : {}),
102
+ fuzzy: serving.fuzzy,
103
+ threshold: serving.threshold,
104
+ ...(tier !== undefined ? { tier } : {}),
105
+ fallbacks: serving.fallbacks,
320
106
  ttlMs: serving.ttlMs,
321
107
  });
322
- const modelId =
323
- explicit !== undefined
324
- ? resolveModelId(explicit, endpoints, { threshold: serving.threshold }).modelId
325
- : pickFirstAvailable(serving.fallbacks, endpoints);
326
- log.debug("model selected", {
327
- modelId,
328
- source: explicit !== undefined ? "fuzzy-match" : "fallback",
329
- requestedExplicit: explicit,
330
- });
331
- return modelId;
332
- }
108
+ log.debug("model selected", { modelId, source, requested });
333
109
 
334
- /**
335
- * Find the first id in `fallbacks` whose endpoint is present in
336
- * `endpoints`. Returns the top fallback when the workspace has none
337
- * of them so callers always get a string; an offline workspace will
338
- * then receive a clean 404 from Databricks instead of a malformed
339
- * config.
340
- */
341
- function pickFirstAvailable(
342
- fallbacks: readonly string[],
343
- endpoints: readonly ServingEndpointSummary[],
344
- ): string {
345
- const present = new Set(endpoints.map((e) => e.name));
346
- for (const candidate of fallbacks) {
347
- if (present.has(candidate)) return candidate;
348
- }
349
- return fallbacks[0] ?? FALLBACK_MODEL_IDS[0]!;
110
+ return {
111
+ providerId: config.providerId ?? "databricks",
112
+ modelId,
113
+ url,
114
+ headers: Object.fromEntries(headers.entries()),
115
+ };
350
116
  }
351
117
 
352
118
  /** Path prefix that identifies a Databricks Model Serving REST call. */