Skip to content

Commit 983aa20

Browse files
roaminroMastra Code (anthropic/claude-opus-4-8)
andauthored
fix(server): scope Agent Builder model list to connected providers (#17725)
Co-authored-by: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
1 parent 18cf934 commit 983aa20

3 files changed

Lines changed: 54 additions & 12 deletions

File tree

.changeset/purple-adults-find.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/server': patch
3+
---
4+
5+
Fixed the Agent Builder selecting models from providers with no API key configured. The builder available-models list now only includes providers whose API key is set, so an agent created in the Agent Builder can no longer be assigned a model that can never run.

packages/server/src/server/handlers/editor-builder.test.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { IAgentBuilder } from '@mastra/core/agent-builder/ee';
22
import type { IMastraEditor } from '@mastra/core/editor';
3-
import { describe, it, expect, vi } from 'vitest';
3+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
44

55
import {
66
GET_EDITOR_BUILDER_AVAILABLE_MODELS_ROUTE,
@@ -613,18 +613,47 @@ describe('GET /editor/builder/infrastructure route metadata', () => {
613613
describe('GET /editor/builder/models/available', () => {
614614
const runAvailable = (editor?: Partial<IMastraEditor>) =>
615615
GET_EDITOR_BUILDER_AVAILABLE_MODELS_ROUTE.handler({ mastra: createMockMastra(editor) } as any) as Promise<{
616-
providers: Array<{ id: string; models: string[] }>;
616+
providers: Array<{ id: string; models: string[]; connected: boolean }>;
617617
}>;
618618

619-
it('returns the full provider list when no editor / policy is configured', async () => {
619+
const originalOpenAIKey = process.env.OPENAI_API_KEY;
620+
const originalAnthropicKey = process.env.ANTHROPIC_API_KEY;
621+
622+
beforeEach(() => {
623+
// The endpoint only surfaces providers with a configured API key, so make
624+
// exactly one provider "connected" and ensure another is not.
625+
process.env.OPENAI_API_KEY = 'test-key';
626+
delete process.env.ANTHROPIC_API_KEY;
627+
});
628+
629+
afterEach(() => {
630+
if (originalOpenAIKey === undefined) delete process.env.OPENAI_API_KEY;
631+
else process.env.OPENAI_API_KEY = originalOpenAIKey;
632+
if (originalAnthropicKey === undefined) delete process.env.ANTHROPIC_API_KEY;
633+
else process.env.ANTHROPIC_API_KEY = originalAnthropicKey;
634+
});
635+
636+
it('returns only providers with a configured API key when no policy is configured', async () => {
620637
const { providers } = await runAvailable();
621638

622639
expect(providers.length).toBeGreaterThan(0);
640+
// Every returned provider is connected (has its API key configured).
641+
expect(providers.every(p => p.connected)).toBe(true);
642+
623643
const openai = providers.find(p => p.id === 'openai');
624644
expect(openai).toBeDefined();
625645
expect(openai!.models.length).toBeGreaterThan(0);
626646
});
627647

648+
it('omits providers whose API key is not configured', async () => {
649+
const { providers } = await runAvailable();
650+
651+
// anthropic has no API key set in this test, so it must be filtered out
652+
// even though it exists in the provider registry.
653+
expect(providers.find(p => p.id === 'anthropic')).toBeUndefined();
654+
expect(providers.find(p => p.id === 'openai')).toBeDefined();
655+
});
656+
628657
it('filters models down to the active policy allowlist (provider wildcard)', async () => {
629658
const builder: IAgentBuilder = {
630659
enabled: true,

packages/server/src/server/handlers/editor-builder.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,18 @@ export const GET_EDITOR_BUILDER_SETTINGS_ROUTE = createRoute({
179179
/**
180180
* GET /editor/builder/models/available
181181
*
182-
* Returns the configured AI providers/models filtered by the active builder
183-
* model policy. The server is the single authority for the allowlist: it
184-
* applies `isModelAllowed` here so the Studio model picker can render the
185-
* response verbatim without importing any EE matcher into the browser.
182+
* Returns the configured AI providers/models the agent builder may use. The
183+
* server is the single authority: it scopes the list to providers with a
184+
* configured API key (`connected`) and applies the active builder model
185+
* policy via `isModelAllowed`, so the Studio surfaces can render the response
186+
* verbatim without importing any EE matcher into the browser.
186187
*
187-
* - Policy inactive (or no allowlist) ⇒ the full provider list is returned.
188-
* - Policy active with an allowlist ⇒ each provider's models are filtered,
189-
* and providers left with no allowed models are omitted entirely.
188+
* - Providers without a configured API key are always omitted — the builder
189+
* decides the agent's model from this list, so an unconnected provider would
190+
* produce an agent that can never run.
191+
* - Policy inactive (or no allowlist) ⇒ all connected providers are returned.
192+
* - Policy active with an allowlist ⇒ each connected provider's models are
193+
* filtered, and providers left with no allowed models are omitted entirely.
190194
*/
191195
export const GET_EDITOR_BUILDER_AVAILABLE_MODELS_ROUTE = createRoute({
192196
method: 'GET',
@@ -200,10 +204,14 @@ export const GET_EDITOR_BUILDER_AVAILABLE_MODELS_ROUTE = createRoute({
200204
requiresPermission: 'stored-agents:read',
201205
handler: async ({ mastra }) => {
202206
try {
203-
const providers = await buildProvidersList(mastra);
207+
// Only surface providers whose API key is configured (`connected`). The
208+
// agent builder decides the agent's model from this list, so including
209+
// providers without a key lets it pick a model that can never run. We
210+
// scope to connected providers so every choice is actually usable.
211+
const providers = (await buildProvidersList(mastra)).filter(provider => provider.connected);
204212
const policy = await resolveBuilderModelPolicy(mastra.getEditor());
205213

206-
// Inactive policy (or no allowlist) ⇒ nothing to filter.
214+
// Inactive policy (or no allowlist) ⇒ no allowlist filtering to apply.
207215
if (!policy.active || !policy.allowed || policy.allowed.length === 0) {
208216
return { providers };
209217
}

0 commit comments

Comments
 (0)