@dbx-tools/appkit-mastra 0.1.41 → 0.1.48

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/dist/src/model.js CHANGED
@@ -7,214 +7,24 @@
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 / class / 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 / class / 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
  */
23
+ import { parseModelClass, selectModel } from "@dbx-tools/model";
27
24
  import { commonUtils, logUtils, netUtils, stringUtils } from "@dbx-tools/shared";
28
25
  import { MASTRA_USER_KEY } from "./config.js";
29
- import { listServingEndpoints, MASTRA_MODEL_OVERRIDE_KEY, resolveModelId, resolveServingConfig, } from "./serving.js";
30
- /**
31
- * Capability tiers for Databricks Foundation Model API endpoints.
32
- *
33
- * - {@link ModelTier.Thinking}: deepest reasoning / "thinking" models
34
- * (Claude Opus, GPT-5.5 Pro, Gemini Pro, Llama 4 Maverick, etc).
35
- * Highest cost and latency; reserve for hard multi-step reasoning.
36
- * - {@link ModelTier.Balanced}: cost/latency sweet spot for general
37
- * agent work (Claude Sonnet, GPT-5.x, Gemini Flash, Llama 3.3 70B).
38
- * The right default for most agents.
39
- * - {@link ModelTier.Fast}: cheap and quick; classification, routing,
40
- * tool-arg extraction, simple summarisation (Claude Haiku, GPT-5
41
- * mini/nano, Gemini Flash Lite, GPT-OSS 20B, Llama 3.1 8B).
42
- *
43
- * String enum so the value is the slug we use in cache keys, logs,
44
- * and as the value users see in serialized configs.
45
- */
46
- export var ModelTier;
47
- (function (ModelTier) {
48
- ModelTier["Thinking"] = "thinking";
49
- ModelTier["Balanced"] = "balanced";
50
- ModelTier["Fast"] = "fast";
51
- })(ModelTier || (ModelTier = {}));
52
- /**
53
- * Catalogue of Databricks-hosted Foundation Model API endpoints,
54
- * grouped by capability {@link ModelTier} and then by provider. Each
55
- * inner array is priority-ordered (most powerful first within the
56
- * same provider+tier).
57
- *
58
- * Provider buckets:
59
- *
60
- * - `claude`: Anthropic Claude family (closed; flagship reasoning).
61
- * - `gpt`: OpenAI GPT-5 family (closed; "ChatGPT" on Databricks FMAPI).
62
- * - `gemini`: Google Gemini family (closed; multimodal + web-search).
63
- * - `openSource`: open-weights models (widest regional / SKU availability).
64
- *
65
- * The list is curated by hand; refresh from the Databricks "supported
66
- * foundation models" doc when new endpoints land.
67
- */
68
- export const MODEL_CATALOG = {
69
- [ModelTier.Thinking]: {
70
- claude: [
71
- "databricks-claude-opus-4-8",
72
- "databricks-claude-opus-4-7",
73
- "databricks-claude-opus-4-6",
74
- "databricks-claude-opus-4-5",
75
- "databricks-claude-opus-4-1",
76
- ],
77
- gpt: ["databricks-gpt-5-5-pro"],
78
- gemini: [
79
- "databricks-gemini-3-1-pro",
80
- "databricks-gemini-3-pro",
81
- "databricks-gemini-2-5-pro",
82
- ],
83
- openSource: [
84
- "databricks-llama-4-maverick",
85
- "databricks-gpt-oss-120b",
86
- "databricks-meta-llama-3-1-405b-instruct",
87
- ],
88
- },
89
- [ModelTier.Balanced]: {
90
- claude: [
91
- "databricks-claude-sonnet-4-6",
92
- "databricks-claude-sonnet-4-5",
93
- "databricks-claude-sonnet-4",
94
- ],
95
- gpt: [
96
- "databricks-gpt-5-5",
97
- "databricks-gpt-5-4",
98
- "databricks-gpt-5-2",
99
- "databricks-gpt-5-1",
100
- "databricks-gpt-5",
101
- ],
102
- gemini: [
103
- "databricks-gemini-3-5-flash",
104
- "databricks-gemini-3-flash",
105
- "databricks-gemini-2-5-flash",
106
- ],
107
- openSource: [
108
- "databricks-meta-llama-3-3-70b-instruct",
109
- "databricks-qwen3-next-80b-a3b-instruct",
110
- "databricks-qwen35-122b-a10b",
111
- ],
112
- },
113
- [ModelTier.Fast]: {
114
- claude: ["databricks-claude-haiku-4-5"],
115
- gpt: [
116
- "databricks-gpt-5-4-mini",
117
- "databricks-gpt-5-4-nano",
118
- "databricks-gpt-5-mini",
119
- "databricks-gpt-5-nano",
120
- ],
121
- gemini: ["databricks-gemini-3-1-flash-lite"],
122
- openSource: [
123
- "databricks-gpt-oss-20b",
124
- "databricks-gemma-3-12b",
125
- "databricks-meta-llama-3-1-8b-instruct",
126
- ],
127
- },
128
- };
129
- /**
130
- * Round-robin zip: take one from each input list in order, skipping
131
- * lists that have already been exhausted. Used to interleave provider
132
- * buckets within a tier so the resolver alternates between vendors
133
- * instead of draining one before trying the next.
134
- *
135
- * Example: `interleave(["a1","a2","a3"], ["b1","b2"])` ->
136
- * `["a1","b1","a2","b2","a3"]`.
137
- */
138
- function interleave(...lists) {
139
- const out = [];
140
- const max = Math.max(0, ...lists.map((l) => l.length));
141
- for (let i = 0; i < max; i++) {
142
- for (const list of lists) {
143
- if (i < list.length)
144
- out.push(list[i]);
145
- }
146
- }
147
- return out;
148
- }
149
- /**
150
- * Priority-ordered model ids for a single capability {@link ModelTier},
151
- * interleaved across providers so a workspace missing the top Claude
152
- * still lands on a flagship GPT / Gemini on the next probe.
153
- *
154
- * Provider order within the interleave: Claude, GPT, Gemini, then the
155
- * open-weights tail appended verbatim as the universal floor (widest
156
- * regional availability).
157
- *
158
- * @example
159
- * ```ts
160
- * mastra({
161
- * defaultModelFallbacks: modelsForTier(ModelTier.Fast),
162
- * });
163
- * ```
164
- */
165
- export function modelsForTier(tier) {
166
- const bucket = MODEL_CATALOG[tier];
167
- return [
168
- ...interleave(bucket.claude, bucket.gpt, bucket.gemini),
169
- ...bucket.openSource,
170
- ];
171
- }
172
- /**
173
- * Top model id at the given {@link ModelTier}. Sync; the agent-step
174
- * resolver fuzzy-matches it against the workspace catalogue at call
175
- * time, so this works even when the literal top pick isn't deployed.
176
- *
177
- * Use when wiring a tier-appropriate model into an agent definition:
178
- *
179
- * @example
180
- * ```ts
181
- * const classifier = createAgent({
182
- * instructions: "Classify this email",
183
- * model: modelForTier(ModelTier.Fast), // cheap, quick
184
- * });
185
- *
186
- * const planner = createAgent({
187
- * instructions: "Plan a multi-step migration",
188
- * model: modelForTier(ModelTier.Thinking), // deep reasoning
189
- * });
190
- * ```
191
- */
192
- export function modelForTier(tier) {
193
- return modelsForTier(tier)[0];
194
- }
195
- /**
196
- * Last-resort model ids used when neither `config.defaultModel`,
197
- * per-agent `model`, nor `DATABRICKS_SERVING_ENDPOINT_NAME` is set.
198
- *
199
- * Walked in order at resolve time: the first id whose endpoint is
200
- * actually present in the workspace's `/serving-endpoints` listing
201
- * wins. Workspaces vary - not every region / SKU has every model,
202
- * and the list of Foundation Model APIs evolves quickly - so the
203
- * resolver degrades all the way from "best thinking model" down to
204
- * "smallest commodity Llama" before giving up.
205
- *
206
- * Built by chaining the per-tier interleaves (Thinking -> Balanced
207
- * -> Fast); within each tier the providers are round-robin-zipped
208
- * (Claude, GPT, Gemini, then open-weights tail). Override the entire
209
- * list via `MastraPluginConfig.defaultModelFallbacks` (e.g. to pin a
210
- * regulated workspace to a specific approved subset, or to bias the
211
- * priority toward a particular tier).
212
- */
213
- export const FALLBACK_MODEL_IDS = [
214
- ...modelsForTier(ModelTier.Thinking),
215
- ...modelsForTier(ModelTier.Balanced),
216
- ...modelsForTier(ModelTier.Fast),
217
- ];
26
+ import { MASTRA_MODEL_OVERRIDE_KEY, resolveServingConfig } from "./serving.js";
27
+ export { classifyEndpoints, FALLBACK_MODEL_IDS, ModelClass, modelForClass, modelsForClass, } from "@dbx-tools/model";
218
28
  /**
219
29
  * Resolve a `MastraModelConfig` for the current agent step. Runs
220
30
  * while `agent.stream` is inside the `asUser(req)` scope so tokens
@@ -232,76 +42,37 @@ export async function buildModel(config, requestContext, overrides = {}) {
232
42
  // URL we hand it. Drop the trailing slash so the resulting URL stays
233
43
  // well-formed (`/serving-endpoints/chat/completions`).
234
44
  const url = new URL("/serving-endpoints", host).toString().replace(/\/$/, "");
235
- const modelId = await pickModelId(config, requestContext, overrides, user, host);
236
- return {
237
- providerId: config.providerId ?? "databricks",
238
- modelId,
239
- url,
240
- headers: Object.fromEntries(headers.entries()),
241
- };
242
- }
243
- /**
244
- * Walk the resolution ladder and pick a modelId.
245
- *
246
- * 1. **Explicit ask** (per-request override, agent `model` string,
247
- * `config.defaultModel` string, or `DATABRICKS_SERVING_ENDPOINT_NAME`):
248
- * when fuzzy matching is on, snap the input to the closest live
249
- * endpoint so loose names like `"claude sonnet"` resolve. When it's
250
- * off (or no endpoint matches within threshold), the input is used
251
- * verbatim and Databricks surfaces the canonical 404.
252
- *
253
- * 2. **No explicit ask**: walk
254
- * {@link MastraPluginConfig.defaultModelFallbacks} (or
255
- * {@link FALLBACK_MODEL_IDS} when unset) and return the first id
256
- * whose endpoint is actually present in the workspace listing. A
257
- * workspace without Claude Opus still gets a sensible default by
258
- * skipping ahead to whichever Sonnet / GPT-5 / Llama variant is
259
- * wired up.
260
- *
261
- * Catalogue fetches fail loud: network / auth errors propagate to the
262
- * caller so they see the real SDK message instead of a silent fallback
263
- * to the top of the priority list.
264
- */
265
- async function pickModelId(config, requestContext, overrides, user, host) {
266
45
  const log = logUtils.logger(config);
267
- const serving = resolveServingConfig(config, FALLBACK_MODEL_IDS);
46
+ const serving = resolveServingConfig(config);
268
47
  const override = serving.allowOverride
269
48
  ? requestContext.get(MASTRA_MODEL_OVERRIDE_KEY)
270
49
  : undefined;
271
- const explicit = override ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
272
- // Cheap exit: when the caller named a specific model and fuzzy
273
- // matching is off, there's no reason to touch the catalogue at all.
274
- if (explicit !== undefined && !serving.fuzzy) {
275
- log.debug("model selected", { modelId: explicit, source: "explicit" });
276
- return explicit;
277
- }
278
- const endpoints = await listServingEndpoints(user.executionContext.client, host, {
50
+ // The override / agent default / env value can be either a concrete
51
+ // endpoint name or a model class slug ("chat-thinking" /
52
+ // "chat-balanced" / "chat-fast"). A class slug becomes a class intent
53
+ // (let the live catalogue pick the best model in that band); anything
54
+ // else is an explicit name fuzzy-matched against the catalogue. An
55
+ // internal `overrides.modelClass` (e.g. the chart planner) is the
56
+ // floor when nothing was requested.
57
+ const requested = override ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
58
+ const requestedClass = requested !== undefined ? parseModelClass(requested) : null;
59
+ const explicit = requestedClass === null ? requested : undefined;
60
+ const modelClass = requestedClass ?? overrides.modelClass;
61
+ const { modelId, source } = await selectModel(user.executionContext.client, host, {
62
+ ...(explicit !== undefined ? { explicit } : {}),
63
+ fuzzy: serving.fuzzy,
64
+ threshold: serving.threshold,
65
+ ...(modelClass !== undefined ? { modelClass } : {}),
66
+ fallbacks: serving.fallbacks,
279
67
  ttlMs: serving.ttlMs,
280
68
  });
281
- const modelId = explicit !== undefined
282
- ? resolveModelId(explicit, endpoints, { threshold: serving.threshold }).modelId
283
- : pickFirstAvailable(serving.fallbacks, endpoints);
284
- log.debug("model selected", {
69
+ log.debug("model selected", { modelId, source, requested });
70
+ return {
71
+ providerId: config.providerId ?? "databricks",
285
72
  modelId,
286
- source: explicit !== undefined ? "fuzzy-match" : "fallback",
287
- requestedExplicit: explicit,
288
- });
289
- return modelId;
290
- }
291
- /**
292
- * Find the first id in `fallbacks` whose endpoint is present in
293
- * `endpoints`. Returns the top fallback when the workspace has none
294
- * of them so callers always get a string; an offline workspace will
295
- * then receive a clean 404 from Databricks instead of a malformed
296
- * config.
297
- */
298
- function pickFirstAvailable(fallbacks, endpoints) {
299
- const present = new Set(endpoints.map((e) => e.name));
300
- for (const candidate of fallbacks) {
301
- if (present.has(candidate))
302
- return candidate;
303
- }
304
- return fallbacks[0] ?? FALLBACK_MODEL_IDS[0];
73
+ url,
74
+ headers: Object.fromEntries(headers.entries()),
75
+ };
305
76
  }
306
77
  /** Path prefix that identifies a Databricks Model Serving REST call. */
307
78
  const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * AppKit plugin that builds one or more Mastra `Agent` instances and
3
- * mounts the `@mastra/express` server plus `@mastra/ai-sdk` `chatRoute`
4
- * handlers. The UI message stream matches what `chatRoute()` emits, so
5
- * the client can use `useChat()` from `@ai-sdk/react` without custom
6
- * parsing.
3
+ * mounts the `@mastra/express` server. Clients drive the conversation
4
+ * over the standard Mastra agent stream (`@mastra/client-js`'s
5
+ * `getAgent(id).stream()`), so there's no bespoke chat transport to
6
+ * keep in sync.
7
7
  *
8
8
  * - Agents: registered through `config.agents` at plugin creation
9
9
  * ({@link MastraAgentDefinition}). Each entry's `tools` field accepts
@@ -21,17 +21,18 @@
21
21
  * `schemaName: "mastra_<agentId>"`; the vector store is a single
22
22
  * shared singleton across every agent.
23
23
  * - Server: the Express subapp wiring lives in `./server.js`.
24
- * - HTTP: AppKit mounts this plugin under `/api/mastra`. `chatRoute`
25
- * is registered at `/route/chat` (bound to `config.defaultAgent` or
26
- * the first registered id) and `/route/chat/:agentId`, so the
27
- * AI SDK transport URL is `/api/mastra/route/chat/<agentId>`.
24
+ * - HTTP: AppKit mounts this plugin under `/api/mastra`. Alongside the
25
+ * Mastra agent routes, the plugin registers `/route/history`
26
+ * (load + clear thread history), `/models`, `/suggestions`, and the
27
+ * generic `/embed/:type/:id` resolver for inline chart / data
28
+ * markers.
28
29
  */
29
30
  import { Plugin, type IAppRouter, type ResourceRequirement } from "@databricks/appkit";
30
31
  import type { Agent } from "@mastra/core/agent";
31
32
  import { Mastra } from "@mastra/core/mastra";
33
+ import { type ServingEndpointSummary } from "@dbx-tools/model";
32
34
  import type { MastraPluginConfig } from "./config.js";
33
35
  import { MastraServer } from "./server.js";
34
- import { type ServingEndpointSummary } from "./serving.js";
35
36
  /**
36
37
  * AppKit plugin (registered name: `mastra`) that hosts Mastra agents
37
38
  * with optional Lakebase-backed memory and AI SDK chat routes under
@@ -97,9 +98,9 @@ export declare class MastraPlugin extends Plugin<MastraPluginConfig> {
97
98
  */
98
99
  get: (id: string) => Agent | null;
99
100
  /**
100
- * The agent `chatRoute` binds to when the client doesn't name
101
- * one. Resolves to `config.defaultAgent`, the first registered
102
- * id, or the built-in `default` fallback.
101
+ * The agent the client converses with when it doesn't name one.
102
+ * Resolves to `config.defaultAgent`, the first registered id, or
103
+ * the built-in `default` fallback.
103
104
  */
104
105
  getDefault: () => Agent | null;
105
106
  /** Underlying Mastra instance for advanced use (custom routes etc.). */
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * AppKit plugin that builds one or more Mastra `Agent` instances and
3
- * mounts the `@mastra/express` server plus `@mastra/ai-sdk` `chatRoute`
4
- * handlers. The UI message stream matches what `chatRoute()` emits, so
5
- * the client can use `useChat()` from `@ai-sdk/react` without custom
6
- * parsing.
3
+ * mounts the `@mastra/express` server. Clients drive the conversation
4
+ * over the standard Mastra agent stream (`@mastra/client-js`'s
5
+ * `getAgent(id).stream()`), so there's no bespoke chat transport to
6
+ * keep in sync.
7
7
  *
8
8
  * - Agents: registered through `config.agents` at plugin creation
9
9
  * ({@link MastraAgentDefinition}). Each entry's `tools` field accepts
@@ -21,16 +21,18 @@
21
21
  * `schemaName: "mastra_<agentId>"`; the vector store is a single
22
22
  * shared singleton across every agent.
23
23
  * - Server: the Express subapp wiring lives in `./server.js`.
24
- * - HTTP: AppKit mounts this plugin under `/api/mastra`. `chatRoute`
25
- * is registered at `/route/chat` (bound to `config.defaultAgent` or
26
- * the first registered id) and `/route/chat/:agentId`, so the
27
- * AI SDK transport URL is `/api/mastra/route/chat/<agentId>`.
24
+ * - HTTP: AppKit mounts this plugin under `/api/mastra`. Alongside the
25
+ * Mastra agent routes, the plugin registers `/route/history`
26
+ * (load + clear thread history), `/models`, `/suggestions`, and the
27
+ * generic `/embed/:type/:id` resolver for inline chart / data
28
+ * markers.
28
29
  */
29
30
  import { genie, getExecutionContext, lakebase, Plugin, toPlugin, } from "@databricks/appkit";
30
31
  import { appkitUtils, commonUtils, logUtils } from "@dbx-tools/shared";
31
- import { chatRoute } from "@mastra/ai-sdk";
32
32
  import { Mastra } from "@mastra/core/mastra";
33
33
  import express from "express";
34
+ import { MASTRA_ROUTES, } from "@dbx-tools/appkit-mastra-shared";
35
+ import { clearServingEndpointsCache, listServingEndpoints, } from "@dbx-tools/model";
34
36
  import { buildAgents, FALLBACK_AGENT_ID } from "./agents.js";
35
37
  import { fetchChart } from "./chart.js";
36
38
  import { collectSpaceSuggestions, resolveGenieSpaces } from "./genie.js";
@@ -38,7 +40,7 @@ import { historyRoute } from "./history.js";
38
40
  import { createMemoryBuilder, createServicePrincipalPool, needsLakebase, } from "./memory.js";
39
41
  import { buildObservability } from "./observability.js";
40
42
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
41
- import { clearServingEndpointsCache, listServingEndpoints, resolveServingConfig, } from "./serving.js";
43
+ import { resolveServingConfig } from "./serving.js";
42
44
  import { fetchStatementData, isStatementNotFoundError, STATEMENT_ROW_CAP, } from "./statement.js";
43
45
  const GENIE_MANIFEST = appkitUtils.data(genie).plugin.manifest;
44
46
  const LAKEBASE_MANIFEST = appkitUtils.data(lakebase).plugin.manifest;
@@ -160,9 +162,9 @@ export class MastraPlugin extends Plugin {
160
162
  */
161
163
  get: (id) => this.built?.agents[id] ?? null,
162
164
  /**
163
- * The agent `chatRoute` binds to when the client doesn't name
164
- * one. Resolves to `config.defaultAgent`, the first registered
165
- * id, or the built-in `default` fallback.
165
+ * The agent the client converses with when it doesn't name one.
166
+ * Resolves to `config.defaultAgent`, the first registered id, or
167
+ * the built-in `default` fallback.
166
168
  */
167
169
  getDefault: () => (this.built && this.built.agents[this.built.defaultAgentId]) ?? null,
168
170
  /** Underlying Mastra instance for advanced use (custom routes etc.). */
@@ -188,22 +190,16 @@ export class MastraPlugin extends Plugin {
188
190
  }
189
191
  clientConfig() {
190
192
  // AppKit mounts every plugin at `/api/<plugin.name>`. `this.name`
191
- // honors `config.name` overrides, so the published paths stay
192
- // accurate if someone remounts the plugin under a custom id.
193
+ // honors `config.name` overrides, so publishing `basePath` is
194
+ // enough for the client to stay correct under a custom mount id -
195
+ // the per-route segments are fixed (`MASTRA_ROUTES`) and the
196
+ // client (`MastraPluginClient`) derives every endpoint from
197
+ // `basePath`.
193
198
  // Return widens to `Record<string, unknown>` to satisfy the
194
199
  // base-class signature; consumers read it through the typed
195
200
  // `MastraClientConfig` shape via `usePluginClientConfig<...>(...)`.
196
- const basePath = `/api/${this.name}`;
197
201
  const config = {
198
- basePath,
199
- chatPath: `${basePath}/route/chat`,
200
- chatPathTemplate: `${basePath}/route/chat/:agentId`,
201
- modelsPath: `${basePath}/models`,
202
- historyPath: `${basePath}/route/history`,
203
- historyPathTemplate: `${basePath}/route/history/:agentId`,
204
- embedPathTemplate: `${basePath}/embed/:type/:id`,
205
- suggestionsPath: `${basePath}/suggestions`,
206
- suggestionsPathTemplate: `${basePath}/suggestions/:agentId`,
202
+ basePath: `/api/${this.name}`,
207
203
  defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
208
204
  agents: Object.keys(this.built?.agents ?? {}),
209
205
  };
@@ -215,7 +211,7 @@ export class MastraPlugin extends Plugin {
215
211
  // be registered before the catch-all that forwards everything to
216
212
  // the Mastra subapp. Errors propagate to Express's default error
217
213
  // handler via `next(err)` so callers see the real SDK message.
218
- router.get("/models", (req, res, next) => {
214
+ router.get(MASTRA_ROUTES.models, (req, res, next) => {
219
215
  this.userScopedSelf(req)
220
216
  .listModels()
221
217
  .then((endpoints) => res.json({ endpoints }))
@@ -267,7 +263,7 @@ export class MastraPlugin extends Plugin {
267
263
  });
268
264
  },
269
265
  };
270
- router.get("/embed/:type/:id", (req, res, next) => {
266
+ router.get(`${MASTRA_ROUTES.embed}/:type/:id`, (req, res, next) => {
271
267
  const type = req.params["type"] ?? "";
272
268
  const id = req.params["id"];
273
269
  const resolve = embedResolvers[type];
@@ -322,8 +318,8 @@ export class MastraPlugin extends Plugin {
322
318
  res.json({ questions: [] });
323
319
  });
324
320
  };
325
- router.get("/suggestions", handleSuggestions);
326
- router.get("/suggestions/:agentId", handleSuggestions);
321
+ router.get(MASTRA_ROUTES.suggestions, handleSuggestions);
322
+ router.get(`${MASTRA_ROUTES.suggestions}/:agentId`, handleSuggestions);
327
323
  router.use((req, res, next) => {
328
324
  if (!this.mastraApp)
329
325
  return res.status(503).end();
@@ -495,20 +491,28 @@ export class MastraPlugin extends Plugin {
495
491
  mastra: this.mastra,
496
492
  prefix: "",
497
493
  customApiRoutes: [
498
- chatRoute({ path: "/route/chat", agent: this.built.defaultAgentId }),
499
- chatRoute({ path: "/route/chat/:agentId" }),
500
494
  // `historyRoute` registers both GET (load) and DELETE
501
495
  // (clear) on the same path, so it returns an array we
502
496
  // splice in.
503
- ...historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
504
- ...historyRoute({ path: "/route/history/:agentId" }),
497
+ ...historyRoute({
498
+ path: MASTRA_ROUTES.history,
499
+ agent: this.built.defaultAgentId,
500
+ }),
501
+ // Assert the `:agentId` template type: the per-package build's
502
+ // NodeNext resolution widens the imported `MASTRA_ROUTES.history`
503
+ // to `string` (the source/bundler typecheck keeps it a literal),
504
+ // which would otherwise drop this out of the dynamic-agent
505
+ // overload and demand a fixed `agent`.
506
+ ...historyRoute({
507
+ path: `${MASTRA_ROUTES.history}/:agentId`,
508
+ }),
505
509
  ],
506
510
  });
507
511
  await this.mastraServer.init();
508
512
  this.log.debug("build:done", {
509
513
  agents: Object.keys(this.built.agents),
510
514
  defaultAgent: this.built.defaultAgentId,
511
- routes: ["/route/chat", "/route/history", "/models"],
515
+ routes: ["/route/history", "/models", "/suggestions", "/embed/:type/:id"],
512
516
  instanceStorage: instanceStorage !== undefined,
513
517
  observability: observability !== undefined ? "mlflow" : "off",
514
518
  });
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Express-layer plumbing for the Mastra plugin: a `MastraServer` that
3
3
  * stamps the per-request `RequestContext`, and a route-patch middleware
4
- * that lets `@mastra/ai-sdk` `chatRoute` work behind an Express mount
5
- * point.
4
+ * that lets the plugin's custom API routes (e.g. `historyRoute`) work
5
+ * behind an Express mount point.
6
6
  */
7
7
  import { type RequestContext } from "@mastra/core/request-context";
8
8
  import { MastraServer as MastraServerExpress } from "@mastra/express";
@@ -35,24 +35,17 @@ export declare class MastraServer extends MastraServerExpress {
35
35
  configureRequestContextModelOverride(req: express.Request, requestContext: RequestContext): void;
36
36
  }
37
37
  /**
38
- * Patches around `@mastra/express`'s custom-route dispatcher so
39
- * `chatRoute` works when `MastraServer` is hosted on an Express subapp
40
- * mounted under a parent path (e.g. `/api/mastra`).
38
+ * Patches around `@mastra/express`'s custom-route dispatcher so the
39
+ * plugin's custom API routes (e.g. `historyRoute`) work when
40
+ * `MastraServer` is hosted on an Express subapp mounted under a parent
41
+ * path (e.g. `/api/mastra`).
41
42
  *
42
- * Two concerns:
43
- *
44
- * 1. The adapter's `registerCustomApiRoutes` matches against `req.path`
45
- * (mount-relative, correct) but dispatches to its internal Hono
46
- * mini-app using `req.originalUrl`, which still contains the parent
47
- * mount prefix. The Hono app registers the literal `chatRoute` paths
48
- * (for example `/route/chat`), so the absolute URL never matches
49
- * until we overwrite `originalUrl` for `/route` and `/route/*` to
50
- * the mount-relative path.
51
- *
52
- * 2. `memory.resource` must be the authenticated user, not whatever the
53
- * client posts. The custom-route forwarder re-serializes `req.body`
54
- * into the Request body it hands Hono, so mutating the parsed body
55
- * here would propagate into `handleChatStream`'s params (kept for
56
- * future use; `express.json()` runs first so `req.body` is parsed).
43
+ * The adapter's `registerCustomApiRoutes` matches against `req.path`
44
+ * (mount-relative, correct) but dispatches to its internal Hono
45
+ * mini-app using `req.originalUrl`, which still contains the parent
46
+ * mount prefix. The Hono app registers the literal route paths
47
+ * (for example `/route/history`), so the absolute URL never matches
48
+ * until we overwrite `originalUrl` for `/route` and `/route/*` to the
49
+ * mount-relative path.
57
50
  */
58
51
  export declare function attachRoutePatchMiddleware(app: express.Express): void;
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Express-layer plumbing for the Mastra plugin: a `MastraServer` that
3
3
  * stamps the per-request `RequestContext`, and a route-patch middleware
4
- * that lets `@mastra/ai-sdk` `chatRoute` work behind an Express mount
5
- * point.
4
+ * that lets the plugin's custom API routes (e.g. `historyRoute`) work
5
+ * behind an Express mount point.
6
6
  */
7
7
  import { getExecutionContext } from "@databricks/appkit";
8
8
  import { httpUtils, logUtils, stringUtils } from "@dbx-tools/shared";
@@ -128,30 +128,23 @@ export class MastraServer extends MastraServerExpress {
128
128
  }
129
129
  }
130
130
  /**
131
- * Patches around `@mastra/express`'s custom-route dispatcher so
132
- * `chatRoute` works when `MastraServer` is hosted on an Express subapp
133
- * mounted under a parent path (e.g. `/api/mastra`).
131
+ * Patches around `@mastra/express`'s custom-route dispatcher so the
132
+ * plugin's custom API routes (e.g. `historyRoute`) work when
133
+ * `MastraServer` is hosted on an Express subapp mounted under a parent
134
+ * path (e.g. `/api/mastra`).
134
135
  *
135
- * Two concerns:
136
- *
137
- * 1. The adapter's `registerCustomApiRoutes` matches against `req.path`
138
- * (mount-relative, correct) but dispatches to its internal Hono
139
- * mini-app using `req.originalUrl`, which still contains the parent
140
- * mount prefix. The Hono app registers the literal `chatRoute` paths
141
- * (for example `/route/chat`), so the absolute URL never matches
142
- * until we overwrite `originalUrl` for `/route` and `/route/*` to
143
- * the mount-relative path.
144
- *
145
- * 2. `memory.resource` must be the authenticated user, not whatever the
146
- * client posts. The custom-route forwarder re-serializes `req.body`
147
- * into the Request body it hands Hono, so mutating the parsed body
148
- * here would propagate into `handleChatStream`'s params (kept for
149
- * future use; `express.json()` runs first so `req.body` is parsed).
136
+ * The adapter's `registerCustomApiRoutes` matches against `req.path`
137
+ * (mount-relative, correct) but dispatches to its internal Hono
138
+ * mini-app using `req.originalUrl`, which still contains the parent
139
+ * mount prefix. The Hono app registers the literal route paths
140
+ * (for example `/route/history`), so the absolute URL never matches
141
+ * until we overwrite `originalUrl` for `/route` and `/route/*` to the
142
+ * mount-relative path.
150
143
  */
151
144
  export function attachRoutePatchMiddleware(app) {
152
145
  app.use((req, _res, next) => {
153
- const isChat = req.path === "/route" || req.path.startsWith("/route/");
154
- if (!isChat)
146
+ const isCustomRoute = req.path === "/route" || req.path.startsWith("/route/");
147
+ if (!isCustomRoute)
155
148
  return next();
156
149
  req.originalUrl = req.path;
157
150
  next();