@dbx-tools/appkit-mastra 0.1.13 → 0.1.14
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 +304 -645
- package/index.ts +46 -38
- package/package.json +58 -45
- package/src/agents.ts +224 -66
- package/src/chart.ts +531 -429
- package/src/config.ts +270 -19
- package/src/filesystems.ts +1090 -0
- package/src/genie.ts +1000 -660
- package/src/history.ts +166 -79
- package/src/mcp.ts +105 -0
- package/src/memory.ts +94 -92
- package/src/mlflow.ts +149 -0
- package/src/model.ts +75 -408
- package/src/observability.ts +121 -69
- package/src/pagination.ts +34 -0
- package/src/plugin.ts +552 -67
- package/src/processors.ts +168 -0
- package/src/rest.ts +67 -0
- package/src/server.ts +232 -45
- package/src/serving-sanitize.ts +167 -0
- package/src/serving.ts +27 -243
- package/src/statement.ts +89 -0
- package/src/storage-schema.ts +41 -0
- package/src/summarize.ts +176 -0
- package/src/threads.ts +338 -0
- package/src/workspaces.ts +346 -0
- package/src/writer.ts +44 -0
- package/tsconfig.json +41 -0
- package/dist/index.d.ts +0 -20
- package/dist/index.js +0 -20
- package/dist/src/agents.d.ts +0 -306
- package/dist/src/agents.js +0 -403
- package/dist/src/chart.d.ts +0 -170
- package/dist/src/chart.js +0 -491
- package/dist/src/config.d.ts +0 -183
- package/dist/src/config.js +0 -12
- package/dist/src/genie.d.ts +0 -131
- package/dist/src/genie.js +0 -630
- package/dist/src/history.d.ts +0 -67
- package/dist/src/history.js +0 -172
- package/dist/src/memory.d.ts +0 -100
- package/dist/src/memory.js +0 -242
- package/dist/src/model.d.ts +0 -159
- package/dist/src/model.js +0 -427
- package/dist/src/observability.d.ts +0 -33
- package/dist/src/observability.js +0 -71
- package/dist/src/plugin.d.ts +0 -130
- package/dist/src/plugin.js +0 -283
- package/dist/src/processors/strip-stale-charts.d.ts +0 -29
- package/dist/src/processors/strip-stale-charts.js +0 -96
- package/dist/src/server.d.ts +0 -46
- package/dist/src/server.js +0 -123
- package/dist/src/serving.d.ts +0 -156
- package/dist/src/serving.js +0 -231
- package/dist/src/tools/email.d.ts +0 -74
- package/dist/src/tools/email.js +0 -122
- package/dist/tsconfig.build.tsbuildinfo +0 -1
- package/src/processors/strip-stale-charts.ts +0 -105
- package/src/tools/email.ts +0 -147
package/src/model.ts
CHANGED
|
@@ -7,242 +7,52 @@
|
|
|
7
7
|
* fresh bearer header, then point Mastra's OpenAI-compatible provider
|
|
8
8
|
* at `/serving-endpoints` on that host.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
*/
|
|
27
23
|
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
logUtils,
|
|
32
|
-
stringUtils,
|
|
33
|
-
} from "@dbx-tools/appkit-shared";
|
|
24
|
+
import { getExecutionContext } from "@databricks/appkit";
|
|
25
|
+
import { classes, resolve } from "@dbx-tools/model";
|
|
26
|
+
import { model } from "@dbx-tools/shared-model";
|
|
34
27
|
import type { MastraModelConfig } from "@mastra/core/llm";
|
|
35
28
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
36
29
|
|
|
37
|
-
import { MASTRA_USER_KEY, type MastraPluginConfig, type User } from "./config
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
resolveModelId,
|
|
42
|
-
resolveServingConfig,
|
|
43
|
-
type ServingEndpointSummary,
|
|
44
|
-
} from "./serving.js";
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Capability tiers for Databricks Foundation Model API endpoints.
|
|
48
|
-
*
|
|
49
|
-
* - {@link ModelTier.Thinking}: deepest reasoning / "thinking" models
|
|
50
|
-
* (Claude Opus, GPT-5.5 Pro, Gemini Pro, Llama 4 Maverick, etc).
|
|
51
|
-
* Highest cost and latency; reserve for hard multi-step reasoning.
|
|
52
|
-
* - {@link ModelTier.Balanced}: cost/latency sweet spot for general
|
|
53
|
-
* agent work (Claude Sonnet, GPT-5.x, Gemini Flash, Llama 3.3 70B).
|
|
54
|
-
* The right default for most agents.
|
|
55
|
-
* - {@link ModelTier.Fast}: cheap and quick; classification, routing,
|
|
56
|
-
* tool-arg extraction, simple summarisation (Claude Haiku, GPT-5
|
|
57
|
-
* mini/nano, Gemini Flash Lite, GPT-OSS 20B, Llama 3.1 8B).
|
|
58
|
-
*
|
|
59
|
-
* String enum so the value is the slug we use in cache keys, logs,
|
|
60
|
-
* and as the value users see in serialized configs.
|
|
61
|
-
*/
|
|
62
|
-
export enum ModelTier {
|
|
63
|
-
Thinking = "thinking",
|
|
64
|
-
Balanced = "balanced",
|
|
65
|
-
Fast = "fast",
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Catalogue of Databricks-hosted Foundation Model API endpoints,
|
|
70
|
-
* grouped by capability {@link ModelTier} and then by provider. Each
|
|
71
|
-
* inner array is priority-ordered (most powerful first within the
|
|
72
|
-
* same provider+tier).
|
|
73
|
-
*
|
|
74
|
-
* Provider buckets:
|
|
75
|
-
*
|
|
76
|
-
* - `claude`: Anthropic Claude family (closed; flagship reasoning).
|
|
77
|
-
* - `gpt`: OpenAI GPT-5 family (closed; "ChatGPT" on Databricks FMAPI).
|
|
78
|
-
* - `gemini`: Google Gemini family (closed; multimodal + web-search).
|
|
79
|
-
* - `openSource`: open-weights models (widest regional / SKU availability).
|
|
80
|
-
*
|
|
81
|
-
* The list is curated by hand; refresh from the Databricks "supported
|
|
82
|
-
* foundation models" doc when new endpoints land.
|
|
83
|
-
*/
|
|
84
|
-
export const MODEL_CATALOG = {
|
|
85
|
-
[ModelTier.Thinking]: {
|
|
86
|
-
claude: [
|
|
87
|
-
"databricks-claude-opus-4-8",
|
|
88
|
-
"databricks-claude-opus-4-7",
|
|
89
|
-
"databricks-claude-opus-4-6",
|
|
90
|
-
"databricks-claude-opus-4-5",
|
|
91
|
-
"databricks-claude-opus-4-1",
|
|
92
|
-
],
|
|
93
|
-
gpt: ["databricks-gpt-5-5-pro"],
|
|
94
|
-
gemini: [
|
|
95
|
-
"databricks-gemini-3-1-pro",
|
|
96
|
-
"databricks-gemini-3-pro",
|
|
97
|
-
"databricks-gemini-2-5-pro",
|
|
98
|
-
],
|
|
99
|
-
openSource: [
|
|
100
|
-
"databricks-llama-4-maverick",
|
|
101
|
-
"databricks-gpt-oss-120b",
|
|
102
|
-
"databricks-meta-llama-3-1-405b-instruct",
|
|
103
|
-
],
|
|
104
|
-
},
|
|
105
|
-
[ModelTier.Balanced]: {
|
|
106
|
-
claude: [
|
|
107
|
-
"databricks-claude-sonnet-4-6",
|
|
108
|
-
"databricks-claude-sonnet-4-5",
|
|
109
|
-
"databricks-claude-sonnet-4",
|
|
110
|
-
],
|
|
111
|
-
gpt: [
|
|
112
|
-
"databricks-gpt-5-5",
|
|
113
|
-
"databricks-gpt-5-4",
|
|
114
|
-
"databricks-gpt-5-2",
|
|
115
|
-
"databricks-gpt-5-1",
|
|
116
|
-
"databricks-gpt-5",
|
|
117
|
-
],
|
|
118
|
-
gemini: [
|
|
119
|
-
"databricks-gemini-3-5-flash",
|
|
120
|
-
"databricks-gemini-3-flash",
|
|
121
|
-
"databricks-gemini-2-5-flash",
|
|
122
|
-
],
|
|
123
|
-
openSource: [
|
|
124
|
-
"databricks-meta-llama-3-3-70b-instruct",
|
|
125
|
-
"databricks-qwen3-next-80b-a3b-instruct",
|
|
126
|
-
"databricks-qwen35-122b-a10b",
|
|
127
|
-
],
|
|
128
|
-
},
|
|
129
|
-
[ModelTier.Fast]: {
|
|
130
|
-
claude: ["databricks-claude-haiku-4-5"],
|
|
131
|
-
gpt: [
|
|
132
|
-
"databricks-gpt-5-4-mini",
|
|
133
|
-
"databricks-gpt-5-4-nano",
|
|
134
|
-
"databricks-gpt-5-mini",
|
|
135
|
-
"databricks-gpt-5-nano",
|
|
136
|
-
],
|
|
137
|
-
gemini: ["databricks-gemini-3-1-flash-lite"],
|
|
138
|
-
openSource: [
|
|
139
|
-
"databricks-gpt-oss-20b",
|
|
140
|
-
"databricks-gemma-3-12b",
|
|
141
|
-
"databricks-meta-llama-3-1-8b-instruct",
|
|
142
|
-
],
|
|
143
|
-
},
|
|
144
|
-
} as const satisfies Record<ModelTier, Record<string, readonly string[]>>;
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Round-robin zip: take one from each input list in order, skipping
|
|
148
|
-
* lists that have already been exhausted. Used to interleave provider
|
|
149
|
-
* buckets within a tier so the resolver alternates between vendors
|
|
150
|
-
* instead of draining one before trying the next.
|
|
151
|
-
*
|
|
152
|
-
* Example: `interleave(["a1","a2","a3"], ["b1","b2"])` ->
|
|
153
|
-
* `["a1","b1","a2","b2","a3"]`.
|
|
154
|
-
*/
|
|
155
|
-
function interleave<T>(...lists: readonly (readonly T[])[]): T[] {
|
|
156
|
-
const out: T[] = [];
|
|
157
|
-
const max = Math.max(0, ...lists.map((l) => l.length));
|
|
158
|
-
for (let i = 0; i < max; i++) {
|
|
159
|
-
for (const list of lists) {
|
|
160
|
-
if (i < list.length) out.push(list[i]!);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return out;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Priority-ordered model ids for a single capability {@link ModelTier},
|
|
168
|
-
* interleaved across providers so a workspace missing the top Claude
|
|
169
|
-
* still lands on a flagship GPT / Gemini on the next probe.
|
|
170
|
-
*
|
|
171
|
-
* Provider order within the interleave: Claude, GPT, Gemini, then the
|
|
172
|
-
* open-weights tail appended verbatim as the universal floor (widest
|
|
173
|
-
* regional availability).
|
|
174
|
-
*
|
|
175
|
-
* @example
|
|
176
|
-
* ```ts
|
|
177
|
-
* mastra({
|
|
178
|
-
* defaultModelFallbacks: modelsForTier(ModelTier.Fast),
|
|
179
|
-
* });
|
|
180
|
-
* ```
|
|
181
|
-
*/
|
|
182
|
-
export function modelsForTier(tier: ModelTier): readonly string[] {
|
|
183
|
-
const bucket = MODEL_CATALOG[tier];
|
|
184
|
-
return [
|
|
185
|
-
...interleave(bucket.claude, bucket.gpt, bucket.gemini),
|
|
186
|
-
...bucket.openSource,
|
|
187
|
-
];
|
|
188
|
-
}
|
|
30
|
+
import { MASTRA_USER_KEY, type MastraPluginConfig, type User } from "./config";
|
|
31
|
+
import { rewriteServingBody } from "./serving-sanitize";
|
|
32
|
+
import { MASTRA_MODEL_OVERRIDE_KEY, resolveServingConfig } from "./serving";
|
|
33
|
+
import { functionModule, log, net } from "@dbx-tools/shared-core";
|
|
189
34
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
* time, so this works even when the literal top pick isn't deployed.
|
|
194
|
-
*
|
|
195
|
-
* Use when wiring a tier-appropriate model into an agent definition:
|
|
196
|
-
*
|
|
197
|
-
* @example
|
|
198
|
-
* ```ts
|
|
199
|
-
* const classifier = createAgent({
|
|
200
|
-
* instructions: "Classify this email",
|
|
201
|
-
* model: modelForTier(ModelTier.Fast), // cheap, quick
|
|
202
|
-
* });
|
|
203
|
-
*
|
|
204
|
-
* const planner = createAgent({
|
|
205
|
-
* instructions: "Plan a multi-step migration",
|
|
206
|
-
* model: modelForTier(ModelTier.Thinking), // deep reasoning
|
|
207
|
-
* });
|
|
208
|
-
* ```
|
|
209
|
-
*/
|
|
210
|
-
export function modelForTier(tier: ModelTier): string {
|
|
211
|
-
return modelsForTier(tier)[0]!;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Last-resort model ids used when neither `config.defaultModel`,
|
|
216
|
-
* per-agent `model`, nor `DATABRICKS_SERVING_ENDPOINT_NAME` is set.
|
|
217
|
-
*
|
|
218
|
-
* Walked in order at resolve time: the first id whose endpoint is
|
|
219
|
-
* actually present in the workspace's `/serving-endpoints` listing
|
|
220
|
-
* wins. Workspaces vary - not every region / SKU has every model,
|
|
221
|
-
* and the list of Foundation Model APIs evolves quickly - so the
|
|
222
|
-
* resolver degrades all the way from "best thinking model" down to
|
|
223
|
-
* "smallest commodity Llama" before giving up.
|
|
224
|
-
*
|
|
225
|
-
* Built by chaining the per-tier interleaves (Thinking -> Balanced
|
|
226
|
-
* -> Fast); within each tier the providers are round-robin-zipped
|
|
227
|
-
* (Claude, GPT, Gemini, then open-weights tail). Override the entire
|
|
228
|
-
* list via `MastraPluginConfig.defaultModelFallbacks` (e.g. to pin a
|
|
229
|
-
* regulated workspace to a specific approved subset, or to bias the
|
|
230
|
-
* priority toward a particular tier).
|
|
231
|
-
*/
|
|
232
|
-
export const FALLBACK_MODEL_IDS: readonly string[] = [
|
|
233
|
-
...modelsForTier(ModelTier.Thinking),
|
|
234
|
-
...modelsForTier(ModelTier.Balanced),
|
|
235
|
-
...modelsForTier(ModelTier.Fast),
|
|
236
|
-
];
|
|
35
|
+
type ModelClass = model.ModelClass;
|
|
36
|
+
const { parseModelClass } = classes;
|
|
37
|
+
const { selectModel } = resolve;
|
|
237
38
|
|
|
238
39
|
/** Optional overrides accepted by {@link buildModel}. */
|
|
239
40
|
export interface BuildModelOverrides {
|
|
240
41
|
/**
|
|
241
42
|
* Static model id from the agent / plugin config (string sugar on
|
|
242
43
|
* `def.model` or `config.defaultModel`). Loses to the per-request
|
|
243
|
-
* override but wins over env / fallback.
|
|
44
|
+
* override but wins over env / class / fallback.
|
|
244
45
|
*/
|
|
245
46
|
modelId?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Chat capability class to resolve when no explicit model id is
|
|
49
|
+
* supplied. Used by internal agents (e.g. the chart planner asks for
|
|
50
|
+
* {@link model.ModelClass.ChatFast}) to express intent without pinning an
|
|
51
|
+
* endpoint name; the live catalogue is classified and the top
|
|
52
|
+
* available model in the class is chosen, falling back to the
|
|
53
|
+
* class's static list when the workspace has none.
|
|
54
|
+
*/
|
|
55
|
+
modelClass?: ModelClass;
|
|
246
56
|
}
|
|
247
57
|
|
|
248
58
|
/**
|
|
@@ -257,8 +67,13 @@ export async function buildModel(
|
|
|
257
67
|
overrides: BuildModelOverrides = {},
|
|
258
68
|
): Promise<MastraModelConfig> {
|
|
259
69
|
void setupFetchInterceptor();
|
|
260
|
-
|
|
261
|
-
|
|
70
|
+
// The chat path stamps the AppKit user on the request context via
|
|
71
|
+
// `MastraServer`. The MCP transport routes don't thread that context
|
|
72
|
+
// into tool execution, so fall back to the ambient execution context
|
|
73
|
+
// (the active OBO scope, or the service principal) when it's absent.
|
|
74
|
+
const user = requestContext.get(MASTRA_USER_KEY) as User | undefined;
|
|
75
|
+
const executionContext = user?.executionContext ?? getExecutionContext();
|
|
76
|
+
const clientConfig = executionContext.client.config;
|
|
262
77
|
const host = (await clientConfig.getHost()).toString();
|
|
263
78
|
const headers = new Headers();
|
|
264
79
|
await clientConfig.authenticate(headers);
|
|
@@ -267,130 +82,66 @@ export async function buildModel(
|
|
|
267
82
|
// well-formed (`/serving-endpoints/chat/completions`).
|
|
268
83
|
const url = new URL("/serving-endpoints", host).toString().replace(/\/$/, "");
|
|
269
84
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
return {
|
|
273
|
-
providerId: config.providerId ?? "databricks",
|
|
274
|
-
modelId,
|
|
275
|
-
url,
|
|
276
|
-
headers: Object.fromEntries(headers.entries()),
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Walk the resolution ladder and pick a modelId.
|
|
282
|
-
*
|
|
283
|
-
* 1. **Explicit ask** (per-request override, agent `model` string,
|
|
284
|
-
* `config.defaultModel` string, or `DATABRICKS_SERVING_ENDPOINT_NAME`):
|
|
285
|
-
* when fuzzy matching is on, snap the input to the closest live
|
|
286
|
-
* endpoint so loose names like `"claude sonnet"` resolve. When it's
|
|
287
|
-
* off (or no endpoint matches within threshold), the input is used
|
|
288
|
-
* verbatim and Databricks surfaces the canonical 404.
|
|
289
|
-
*
|
|
290
|
-
* 2. **No explicit ask**: walk
|
|
291
|
-
* {@link MastraPluginConfig.defaultModelFallbacks} (or
|
|
292
|
-
* {@link FALLBACK_MODEL_IDS} when unset) and return the first id
|
|
293
|
-
* whose endpoint is actually present in the workspace listing. A
|
|
294
|
-
* workspace without Claude Opus still gets a sensible default by
|
|
295
|
-
* skipping ahead to whichever Sonnet / GPT-5 / Llama variant is
|
|
296
|
-
* wired up.
|
|
297
|
-
*
|
|
298
|
-
* Catalogue fetches fail loud: network / auth errors propagate to the
|
|
299
|
-
* caller so they see the real SDK message instead of a silent fallback
|
|
300
|
-
* to the top of the priority list.
|
|
301
|
-
*/
|
|
302
|
-
async function pickModelId(
|
|
303
|
-
config: MastraPluginConfig,
|
|
304
|
-
requestContext: RequestContext,
|
|
305
|
-
overrides: BuildModelOverrides,
|
|
306
|
-
user: User,
|
|
307
|
-
host: string,
|
|
308
|
-
): Promise<string> {
|
|
309
|
-
const log = logUtils.logger(config);
|
|
310
|
-
const serving = resolveServingConfig(config, FALLBACK_MODEL_IDS);
|
|
85
|
+
const logger = log.logger(config);
|
|
86
|
+
const serving = resolveServingConfig(config);
|
|
311
87
|
const override = serving.allowOverride
|
|
312
88
|
? (requestContext.get(MASTRA_MODEL_OVERRIDE_KEY) as string | undefined)
|
|
313
89
|
: undefined;
|
|
314
|
-
const explicit =
|
|
315
|
-
override ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
|
|
316
|
-
|
|
317
|
-
// Cheap exit: when the caller named a specific model and fuzzy
|
|
318
|
-
// matching is off, there's no reason to touch the catalogue at all.
|
|
319
|
-
if (explicit !== undefined && !serving.fuzzy) {
|
|
320
|
-
log.debug("model selected", { modelId: explicit, source: "explicit" });
|
|
321
|
-
return explicit;
|
|
322
|
-
}
|
|
323
90
|
|
|
324
|
-
|
|
91
|
+
// The override / agent default / env value can be either a concrete
|
|
92
|
+
// endpoint name or a model class slug ("chat-thinking" /
|
|
93
|
+
// "chat-balanced" / "chat-fast"). A class slug becomes a class intent
|
|
94
|
+
// (let the live catalogue pick the best model in that band); anything
|
|
95
|
+
// else is an explicit name fuzzy-matched against the catalogue. An
|
|
96
|
+
// internal `overrides.modelClass` (e.g. the chart planner) is the
|
|
97
|
+
// floor when nothing was requested.
|
|
98
|
+
const requested = override ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
|
|
99
|
+
const requestedClass = requested !== undefined ? parseModelClass(requested) : null;
|
|
100
|
+
const explicit = requestedClass === null ? requested : undefined;
|
|
101
|
+
const modelClass = requestedClass ?? overrides.modelClass;
|
|
102
|
+
|
|
103
|
+
const { modelId, source } = await selectModel(executionContext.client, host, {
|
|
104
|
+
...(explicit !== undefined ? { explicit } : {}),
|
|
105
|
+
fuzzy: serving.fuzzy,
|
|
106
|
+
threshold: serving.threshold,
|
|
107
|
+
...(modelClass !== undefined ? { modelClass } : {}),
|
|
108
|
+
fallbacks: serving.fallbacks,
|
|
325
109
|
ttlMs: serving.ttlMs,
|
|
326
110
|
});
|
|
327
|
-
|
|
328
|
-
explicit !== undefined
|
|
329
|
-
? resolveModelId(explicit, endpoints, { threshold: serving.threshold }).modelId
|
|
330
|
-
: pickFirstAvailable(serving.fallbacks, endpoints);
|
|
331
|
-
log.debug("model selected", {
|
|
332
|
-
modelId,
|
|
333
|
-
source: explicit !== undefined ? "fuzzy-match" : "fallback",
|
|
334
|
-
requestedExplicit: explicit,
|
|
335
|
-
});
|
|
336
|
-
return modelId;
|
|
337
|
-
}
|
|
111
|
+
logger.debug("model selected", { modelId, source, requested });
|
|
338
112
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
*/
|
|
346
|
-
function pickFirstAvailable(
|
|
347
|
-
fallbacks: readonly string[],
|
|
348
|
-
endpoints: readonly ServingEndpointSummary[],
|
|
349
|
-
): string {
|
|
350
|
-
const present = new Set(endpoints.map((e) => e.name));
|
|
351
|
-
for (const candidate of fallbacks) {
|
|
352
|
-
if (present.has(candidate)) return candidate;
|
|
353
|
-
}
|
|
354
|
-
return fallbacks[0] ?? FALLBACK_MODEL_IDS[0]!;
|
|
113
|
+
return {
|
|
114
|
+
providerId: config.providerId ?? "databricks",
|
|
115
|
+
modelId,
|
|
116
|
+
url,
|
|
117
|
+
headers: Object.fromEntries(headers.entries()),
|
|
118
|
+
};
|
|
355
119
|
}
|
|
356
120
|
|
|
357
121
|
/** Path prefix that identifies a Databricks Model Serving REST call. */
|
|
358
122
|
const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
|
|
359
123
|
|
|
360
|
-
/**
|
|
361
|
-
* OpenAI-flavoured chat message shape we need to mutate. We do not
|
|
362
|
-
* import the OpenAI / AI SDK types because both packages keep these
|
|
363
|
-
* fields under internal namespaces; the wire payload is the contract
|
|
364
|
-
* here and it's stable enough to inline.
|
|
365
|
-
*/
|
|
366
|
-
interface ChatMessage {
|
|
367
|
-
role: "system" | "user" | "assistant" | "tool";
|
|
368
|
-
content?: string;
|
|
369
|
-
tool_calls?: Array<{ id: string; type: string; function: unknown }>;
|
|
370
|
-
tool_call_id?: string;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
124
|
/**
|
|
374
125
|
* Install a single shared `globalThis.fetch` wrapper for every POST to
|
|
375
126
|
* `/serving-endpoints/...`. The wrapper does two things:
|
|
376
127
|
*
|
|
377
128
|
* 1. Rewrites the outgoing `messages` array to repair Mastra/AI SDK
|
|
378
129
|
* stream-replay quirks that Databricks-hosted Claude rejects (see
|
|
379
|
-
* {@link
|
|
130
|
+
* {@link rewriteServingBody} in `./serving-sanitize.js`).
|
|
380
131
|
* 2. At `LOG_LEVEL=debug`, dumps the (post-sanitize) JSON body so
|
|
381
132
|
* 4xx debugging doesn't have to fight AI SDK's `[Array]`
|
|
382
133
|
* formatter.
|
|
383
134
|
*
|
|
384
|
-
* Safe to call from any hot path: {@link
|
|
135
|
+
* Safe to call from any hot path: {@link functionModule.memoize} ensures
|
|
385
136
|
* the wrapper is installed at most once per process, so subsequent
|
|
386
|
-
* calls
|
|
387
|
-
*
|
|
137
|
+
* calls are a no-op even when {@link buildModel} fires on every agent
|
|
138
|
+
* step.
|
|
388
139
|
*/
|
|
389
|
-
const setupFetchInterceptor =
|
|
390
|
-
const
|
|
140
|
+
const setupFetchInterceptor = functionModule.memoize((): void => {
|
|
141
|
+
const logger = log.logger("mastra/llm");
|
|
391
142
|
const original = globalThis.fetch.bind(globalThis);
|
|
392
143
|
globalThis.fetch = (async (input, init) => {
|
|
393
|
-
const url =
|
|
144
|
+
const url = net.urlBuilder(input);
|
|
394
145
|
if (
|
|
395
146
|
!url ||
|
|
396
147
|
!url.pathname.startsWith(SERVING_ENDPOINTS_PATH_PREFIX) ||
|
|
@@ -403,94 +154,10 @@ const setupFetchInterceptor = commonUtils.memoize((): void => {
|
|
|
403
154
|
init = { ...init, body: rewritten };
|
|
404
155
|
}
|
|
405
156
|
try {
|
|
406
|
-
|
|
157
|
+
logger.debug("POST", { url: url.toString(), body: JSON.parse(rewritten) });
|
|
407
158
|
} catch {
|
|
408
|
-
|
|
159
|
+
logger.debug("POST", { url: url.toString(), bodyType: "non-JSON" });
|
|
409
160
|
}
|
|
410
161
|
return original(input, init);
|
|
411
162
|
}) as typeof globalThis.fetch;
|
|
412
163
|
});
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Parse, sanitize, and re-serialize a `/serving-endpoints/...` POST
|
|
416
|
-
* body. Returns the original string verbatim when the body is not
|
|
417
|
-
* JSON, has no `messages`, or no rewrite was needed; this lets the
|
|
418
|
-
* caller skip the allocation of a new `init` object in the common
|
|
419
|
-
* pass-through case.
|
|
420
|
-
*/
|
|
421
|
-
function rewriteServingBody(body: string): string {
|
|
422
|
-
let parsed: { messages?: unknown };
|
|
423
|
-
try {
|
|
424
|
-
parsed = JSON.parse(body);
|
|
425
|
-
} catch {
|
|
426
|
-
return body;
|
|
427
|
-
}
|
|
428
|
-
if (!Array.isArray(parsed.messages)) return body;
|
|
429
|
-
const changed = sanitizeServingMessages(parsed.messages as ChatMessage[]);
|
|
430
|
-
return changed ? JSON.stringify(parsed) : body;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Repair a Mastra/AI SDK message replay that Databricks-hosted Claude
|
|
435
|
-
* rejects with `"This model does not support assistant message
|
|
436
|
-
* prefill. The conversation must end with a user message."`.
|
|
437
|
-
*
|
|
438
|
-
* The bug pattern: when an assistant turn streams text *and* a
|
|
439
|
-
* `tool_call`, the AI SDK persists them as two separate assistant
|
|
440
|
-
* entries (text-only and tool-call-only). On the next agent step the
|
|
441
|
-
* tool-call entry is replayed *before* the tool result and the
|
|
442
|
-
* text entry is replayed *after* it, so the conversation ends with a
|
|
443
|
-
* trailing assistant text message. Anthropic interprets that as a
|
|
444
|
-
* prefill request and rejects it on Databricks (the upstream Bedrock
|
|
445
|
-
* route disallows prefill).
|
|
446
|
-
*
|
|
447
|
-
* Fix: when the last message is an assistant text with no `tool_calls`
|
|
448
|
-
* and the chain immediately before it is `assistant(tool_calls=...)`
|
|
449
|
-
* followed only by `tool(...)` results, fold the trailing text back
|
|
450
|
-
* into the `content` of that opening assistant and drop the duplicate.
|
|
451
|
-
* The result is the canonical OpenAI shape
|
|
452
|
-
* `[..., user, assistant(text + tool_calls), tool(...)]` which both
|
|
453
|
-
* Databricks Claude and every other endpoint accept.
|
|
454
|
-
*
|
|
455
|
-
* Mutates `messages` in place; returns `true` when something changed
|
|
456
|
-
* so the caller knows whether to re-serialize.
|
|
457
|
-
*/
|
|
458
|
-
function sanitizeServingMessages(messages: ChatMessage[]): boolean {
|
|
459
|
-
if (messages.length < 2) return false;
|
|
460
|
-
const last = messages[messages.length - 1];
|
|
461
|
-
if (
|
|
462
|
-
!last ||
|
|
463
|
-
last.role !== "assistant" ||
|
|
464
|
-
(last.tool_calls && last.tool_calls.length > 0)
|
|
465
|
-
) {
|
|
466
|
-
return false;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Walk back through any contiguous tool-result messages to find the
|
|
470
|
-
// assistant turn that opened this tool sequence.
|
|
471
|
-
let i = messages.length - 2;
|
|
472
|
-
while (i >= 0 && messages[i]?.role === "tool") i--;
|
|
473
|
-
if (i < 0) return false;
|
|
474
|
-
const opener = messages[i];
|
|
475
|
-
if (
|
|
476
|
-
!opener ||
|
|
477
|
-
opener.role !== "assistant" ||
|
|
478
|
-
!opener.tool_calls ||
|
|
479
|
-
opener.tool_calls.length === 0
|
|
480
|
-
) {
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// `trimToNull` collapses the `typeof string && trimmed` dance and
|
|
485
|
-
// drops blank fragments before the `\n\n` join below, so the merge
|
|
486
|
-
// never introduces stray leading / trailing whitespace.
|
|
487
|
-
const merged = [
|
|
488
|
-
stringUtils.trimToNull(opener.content),
|
|
489
|
-
stringUtils.trimToNull(last.content),
|
|
490
|
-
]
|
|
491
|
-
.filter((s): s is string => s !== null)
|
|
492
|
-
.join("\n\n");
|
|
493
|
-
opener.content = merged;
|
|
494
|
-
messages.pop();
|
|
495
|
-
return true;
|
|
496
|
-
}
|