@dbx-tools/appkit-mastra 0.1.40 → 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.
@@ -26,18 +26,46 @@
26
26
  * (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
27
27
  * is registered); per-agent settings cascade on top of that.
28
28
  */
29
- import { lakebase } from "@databricks/appkit";
30
- import { appkitUtils, logUtils } from "@dbx-tools/shared";
29
+ import { getUsernameWithApiLookup } from "@databricks/appkit";
30
+ import { logUtils } from "@dbx-tools/shared";
31
31
  import { fastembed } from "@mastra/fastembed";
32
32
  import { Memory } from "@mastra/memory";
33
33
  import { PgVector, PostgresStore } from "@mastra/pg";
34
34
  import { randomUUID } from "node:crypto";
35
+ import { Pool } from "pg";
35
36
  const log = logUtils.logger("mastra/memory");
37
+ /**
38
+ * Build a dedicated **service-principal** Lakebase pool for Mastra
39
+ * memory from the lakebase plugin's resolved SP pg config.
40
+ *
41
+ * The plugin's `exports().pool` is a `RoutingPool` that switches to
42
+ * the per-user (OBO) pool whenever a query runs inside an `asUser`
43
+ * scope - exactly the context the mastra plugin establishes around
44
+ * every chat turn. Memory (threads / messages + semantic recall) must
45
+ * instead always act as the app service principal: it owns the
46
+ * auto-created `mastra_*` schemas (a per-user role usually can't
47
+ * `CREATE SCHEMA`) and is shared across users, so it cannot inherit a
48
+ * request's OBO identity.
49
+ *
50
+ * `pgConfig` must be the plugin's `exports().getPgConfig()` evaluated
51
+ * **outside** any `asUser` scope (i.e. during setup), so it carries
52
+ * the SP connection target, OAuth token-refresh `password` callback,
53
+ * and any `lakebase({ pool })` tuning overrides - all of which this
54
+ * pool inherits. See the call site in `plugin.ts`.
55
+ */
56
+ export async function createServicePrincipalPool(pgConfig) {
57
+ // `getPgConfig()` resolves the SP username synchronously from
58
+ // `PGUSER` / `DATABRICKS_CLIENT_ID`; fall back to the async API
59
+ // lookup (e.g. local dev authenticating via PAT) so the pool always
60
+ // has an identity to connect with.
61
+ const user = pgConfig.user ?? (await getUsernameWithApiLookup());
62
+ return new Pool({ ...pgConfig, user });
63
+ }
36
64
  /**
37
65
  * True when any plugin-level or per-agent setting could need the
38
- * Lakebase pool. Used by `plugin.ts` to gate pool acquisition; the
39
- * builder also acquires lazily so missed cases still fail with a
40
- * clear lakebase-not-registered error.
66
+ * Lakebase pool. Used by `plugin.ts` to gate creation of the
67
+ * service-principal pool and the {@link MemoryBuilder} that consumes
68
+ * it; when false neither is built.
41
69
  */
42
70
  export function needsLakebase(config) {
43
71
  if (settingNeedsSharedPool(config.storage))
@@ -48,35 +76,26 @@ export function needsLakebase(config) {
48
76
  return defs.some((d) => settingNeedsSharedPool(d.storage) || settingNeedsSharedPool(d.memory));
49
77
  }
50
78
  /**
51
- * Look up the `lakebase` plugin and return its managed `pg.Pool`.
52
- * Throws when the sibling plugin is not registered; enabling
53
- * `storage` / `memory` without lakebase is a wiring bug, not a runtime
54
- * condition we can recover from.
79
+ * Construct a per-agent {@link Memory} factory bound to the supplied
80
+ * service-principal pool (see {@link createServicePrincipalPool}).
81
+ * Caches the shared `PgVector` singleton (built on first need) so each
82
+ * agent build is O(1) after the first.
55
83
  */
56
- export function resolveLakebasePool(context, caller) {
57
- return appkitUtils.require(context, lakebase, caller).exports().pool;
84
+ export function createMemoryBuilder(config, servicePrincipalPool) {
85
+ return new MemoryBuilder(config, servicePrincipalPool);
58
86
  }
59
87
  /**
60
- * Construct a per-agent {@link Memory} factory. Caches the shared
61
- * `PgVector` singleton (built on first need) and the lazily-resolved
62
- * Lakebase pool so each agent build is O(1) after the first.
63
- */
64
- export function createMemoryBuilder(config, context) {
65
- return new MemoryBuilder(config, context);
66
- }
67
- /**
68
- * Builds one `Memory` per agent. Per-instance state keeps the shared
69
- * `PgVector` and the resolved Lakebase pool alive across calls so
70
- * registering N agents stays cheap.
88
+ * Builds one `Memory` per agent against a shared service-principal
89
+ * Lakebase pool. Per-instance state keeps the shared `PgVector` alive
90
+ * across calls so registering N agents stays cheap.
71
91
  */
72
92
  export class MemoryBuilder {
73
93
  config;
74
- context;
94
+ servicePrincipalPool;
75
95
  sharedVector;
76
- pool;
77
- constructor(config, context) {
96
+ constructor(config, servicePrincipalPool) {
78
97
  this.config = config;
79
- this.context = context;
98
+ this.servicePrincipalPool = servicePrincipalPool;
80
99
  }
81
100
  /**
82
101
  * Build a `Memory` for `agentId` after the plugin/agent cascade.
@@ -105,7 +124,7 @@ export class MemoryBuilder {
105
124
  return new PostgresStore({
106
125
  id: "mastra-store__instance",
107
126
  schemaName: "mastra_instance",
108
- pool: this.requirePool(),
127
+ pool: this.servicePrincipalPool,
109
128
  });
110
129
  }
111
130
  forAgent(agentId, def) {
@@ -143,7 +162,7 @@ export class MemoryBuilder {
143
162
  return new PostgresStore({
144
163
  id: `mastra-store__${agentId}`,
145
164
  schemaName: `mastra_${agentId}`,
146
- pool: this.requirePool(),
165
+ pool: this.servicePrincipalPool,
147
166
  });
148
167
  }
149
168
  // Cast: `withId` guarantees `id` is set, but the distributive
@@ -169,16 +188,10 @@ export class MemoryBuilder {
169
188
  }
170
189
  getSharedVector() {
171
190
  if (!this.sharedVector) {
172
- this.sharedVector = buildSharedPgVector(this.requirePool());
191
+ this.sharedVector = buildSharedPgVector(this.servicePrincipalPool);
173
192
  }
174
193
  return this.sharedVector;
175
194
  }
176
- requirePool() {
177
- if (!this.pool) {
178
- this.pool = resolveLakebasePool(this.context, this.config);
179
- }
180
- return this.pool;
181
- }
182
195
  }
183
196
  /**
184
197
  * Build the shared `PgVector` that backs the default
@@ -7,148 +7,41 @@
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
  */
23
+ import { type ModelTier } from "@dbx-tools/model";
27
24
  import type { MastraModelConfig } from "@mastra/core/llm";
28
25
  import type { RequestContext } from "@mastra/core/request-context";
29
26
  import { type MastraPluginConfig } from "./config.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 declare enum ModelTier {
47
- Thinking = "thinking",
48
- Balanced = "balanced",
49
- Fast = "fast"
50
- }
51
- /**
52
- * Catalogue of Databricks-hosted Foundation Model API endpoints,
53
- * grouped by capability {@link ModelTier} and then by provider. Each
54
- * inner array is priority-ordered (most powerful first within the
55
- * same provider+tier).
56
- *
57
- * Provider buckets:
58
- *
59
- * - `claude`: Anthropic Claude family (closed; flagship reasoning).
60
- * - `gpt`: OpenAI GPT-5 family (closed; "ChatGPT" on Databricks FMAPI).
61
- * - `gemini`: Google Gemini family (closed; multimodal + web-search).
62
- * - `openSource`: open-weights models (widest regional / SKU availability).
63
- *
64
- * The list is curated by hand; refresh from the Databricks "supported
65
- * foundation models" doc when new endpoints land.
66
- */
67
- export declare const MODEL_CATALOG: {
68
- readonly thinking: {
69
- readonly claude: readonly ["databricks-claude-opus-4-8", "databricks-claude-opus-4-7", "databricks-claude-opus-4-6", "databricks-claude-opus-4-5", "databricks-claude-opus-4-1"];
70
- readonly gpt: readonly ["databricks-gpt-5-5-pro"];
71
- readonly gemini: readonly ["databricks-gemini-3-1-pro", "databricks-gemini-3-pro", "databricks-gemini-2-5-pro"];
72
- readonly openSource: readonly ["databricks-llama-4-maverick", "databricks-gpt-oss-120b", "databricks-meta-llama-3-1-405b-instruct"];
73
- };
74
- readonly balanced: {
75
- readonly claude: readonly ["databricks-claude-sonnet-4-6", "databricks-claude-sonnet-4-5", "databricks-claude-sonnet-4"];
76
- readonly gpt: readonly ["databricks-gpt-5-5", "databricks-gpt-5-4", "databricks-gpt-5-2", "databricks-gpt-5-1", "databricks-gpt-5"];
77
- readonly gemini: readonly ["databricks-gemini-3-5-flash", "databricks-gemini-3-flash", "databricks-gemini-2-5-flash"];
78
- readonly openSource: readonly ["databricks-meta-llama-3-3-70b-instruct", "databricks-qwen3-next-80b-a3b-instruct", "databricks-qwen35-122b-a10b"];
79
- };
80
- readonly fast: {
81
- readonly claude: readonly ["databricks-claude-haiku-4-5"];
82
- readonly gpt: readonly ["databricks-gpt-5-4-mini", "databricks-gpt-5-4-nano", "databricks-gpt-5-mini", "databricks-gpt-5-nano"];
83
- readonly gemini: readonly ["databricks-gemini-3-1-flash-lite"];
84
- readonly openSource: readonly ["databricks-gpt-oss-20b", "databricks-gemma-3-12b", "databricks-meta-llama-3-1-8b-instruct"];
85
- };
86
- };
87
- /**
88
- * Priority-ordered model ids for a single capability {@link ModelTier},
89
- * interleaved across providers so a workspace missing the top Claude
90
- * still lands on a flagship GPT / Gemini on the next probe.
91
- *
92
- * Provider order within the interleave: Claude, GPT, Gemini, then the
93
- * open-weights tail appended verbatim as the universal floor (widest
94
- * regional availability).
95
- *
96
- * @example
97
- * ```ts
98
- * mastra({
99
- * defaultModelFallbacks: modelsForTier(ModelTier.Fast),
100
- * });
101
- * ```
102
- */
103
- export declare function modelsForTier(tier: ModelTier): readonly string[];
104
- /**
105
- * Top model id at the given {@link ModelTier}. Sync; the agent-step
106
- * resolver fuzzy-matches it against the workspace catalogue at call
107
- * time, so this works even when the literal top pick isn't deployed.
108
- *
109
- * Use when wiring a tier-appropriate model into an agent definition:
110
- *
111
- * @example
112
- * ```ts
113
- * const classifier = createAgent({
114
- * instructions: "Classify this email",
115
- * model: modelForTier(ModelTier.Fast), // cheap, quick
116
- * });
117
- *
118
- * const planner = createAgent({
119
- * instructions: "Plan a multi-step migration",
120
- * model: modelForTier(ModelTier.Thinking), // deep reasoning
121
- * });
122
- * ```
123
- */
124
- export declare function modelForTier(tier: ModelTier): string;
125
- /**
126
- * Last-resort model ids used when neither `config.defaultModel`,
127
- * per-agent `model`, nor `DATABRICKS_SERVING_ENDPOINT_NAME` is set.
128
- *
129
- * Walked in order at resolve time: the first id whose endpoint is
130
- * actually present in the workspace's `/serving-endpoints` listing
131
- * wins. Workspaces vary - not every region / SKU has every model,
132
- * and the list of Foundation Model APIs evolves quickly - so the
133
- * resolver degrades all the way from "best thinking model" down to
134
- * "smallest commodity Llama" before giving up.
135
- *
136
- * Built by chaining the per-tier interleaves (Thinking -> Balanced
137
- * -> Fast); within each tier the providers are round-robin-zipped
138
- * (Claude, GPT, Gemini, then open-weights tail). Override the entire
139
- * list via `MastraPluginConfig.defaultModelFallbacks` (e.g. to pin a
140
- * regulated workspace to a specific approved subset, or to bias the
141
- * priority toward a particular tier).
142
- */
143
- export declare const FALLBACK_MODEL_IDS: readonly string[];
27
+ export { classifyEndpoints, FALLBACK_MODEL_IDS, modelForTier, modelsForTier, ModelTier, } from "@dbx-tools/model";
144
28
  /** Optional overrides accepted by {@link buildModel}. */
145
29
  export interface BuildModelOverrides {
146
30
  /**
147
31
  * Static model id from the agent / plugin config (string sugar on
148
32
  * `def.model` or `config.defaultModel`). Loses to the per-request
149
- * override but wins over env / fallback.
33
+ * override but wins over env / tier / fallback.
150
34
  */
151
35
  modelId?: string;
36
+ /**
37
+ * Capability tier to resolve when no explicit model id is supplied.
38
+ * Used by internal agents (e.g. the chart planner asks for
39
+ * {@link ModelTier.Fast}) to express intent without pinning an
40
+ * endpoint name; the live catalogue is classified and the top
41
+ * available model in the tier is chosen, falling back to the
42
+ * tier's static list when the workspace has none.
43
+ */
44
+ tier?: ModelTier;
152
45
  }
153
46
  /**
154
47
  * Resolve a `MastraModelConfig` for the current agent step. Runs
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 / 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
  */
23
+ import { parseModelTier, 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, modelForTier, modelsForTier, ModelTier, } 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 capability tier slug ("thinking" / "balanced" /
52
+ // "fast"). A tier slug becomes a tier intent (let the live catalogue
53
+ // pick the best model in that band); anything else is an explicit
54
+ // name fuzzy-matched against the catalogue. An internal
55
+ // `overrides.tier` (e.g. the chart planner) is the floor when nothing
56
+ // was requested.
57
+ const requested = override ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
58
+ const requestedTier = requested !== undefined ? parseModelTier(requested) : null;
59
+ const explicit = requestedTier === null ? requested : undefined;
60
+ const tier = requestedTier ?? overrides.tier;
61
+ const { modelId, source } = await selectModel(user.executionContext.client, host, {
62
+ ...(explicit !== undefined ? { explicit } : {}),
63
+ fuzzy: serving.fuzzy,
64
+ threshold: serving.threshold,
65
+ ...(tier !== undefined ? { tier } : {}),
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/";