Skip to content

Commit 3bc544f

Browse files
committed
feat(core): strip unsupported sampling params via model capability registry
Some models (claude-opus-4-7, gpt-5-pro) reject temperature/topP/topK with a 400. The model router now checks the capability registry before dispatching and silently strips unsupported sampling parameters. Extends the existing attachment-only capability system to a generic multi-dimension registry. The temperature dimension is populated from models.dev data and auto-updated by the CI regeneration workflow. Closes #16247
1 parent 848faa0 commit 3bc544f

13 files changed

Lines changed: 387 additions & 42 deletions

File tree

.changeset/twelve-candles-love.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@mastra/core': minor
3+
---
4+
5+
Agents using models that dropped support for `temperature`, `topP`, or `topK` (such as `claude-opus-4-7` or `gpt-5-pro`) no longer crash with a 400 error. The model router now automatically strips unsupported sampling parameters before the request is sent — no configuration or processors needed.
6+
7+
```ts
8+
const agent = new Agent({
9+
model: 'anthropic/claude-opus-4-7',
10+
instructions: 'You are a helpful assistant.',
11+
});
12+
13+
// temperature is stripped automatically — no 400 error
14+
await agent.generate('hello', { modelSettings: { temperature: 0.7 } });
15+
```

packages/core/scripts/generate-providers.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const __dirname = path.dirname(__filename);
1010

1111
async function generateProviderRegistry(gateways: MastraModelGateway[]) {
1212
// Fetch providers from all gateways
13-
const { providers, models, attachmentCapabilities, failedGateways } = await fetchProvidersFromGateways(gateways);
13+
const { providers, models, attachmentCapabilities, temperatureCapabilities, failedGateways } =
14+
await fetchProvidersFromGateways(gateways);
1415

1516
if (failedGateways.length > 0) {
1617
console.warn(
@@ -23,12 +24,26 @@ async function generateProviderRegistry(gateways: MastraModelGateway[]) {
2324
const srcDir = path.join(__dirname, '..', 'src', 'llm', 'model');
2425
const srcJsonPath = path.join(srcDir, 'provider-registry.json');
2526
const srcTypesPath = path.join(srcDir, 'provider-types.generated.d.ts');
26-
await writeRegistryFiles(srcJsonPath, srcTypesPath, providers, models, attachmentCapabilities);
27+
await writeRegistryFiles(
28+
srcJsonPath,
29+
srcTypesPath,
30+
providers,
31+
models,
32+
attachmentCapabilities,
33+
temperatureCapabilities,
34+
);
2735

2836
// Write registry files to dist/ (for build output)
2937
const distJsonPath = path.join(__dirname, '..', 'dist', 'provider-registry.json');
3038
const distTypesPath = path.join(__dirname, '..', 'dist', 'llm', 'model', 'provider-types.generated.d.ts');
31-
await writeRegistryFiles(distJsonPath, distTypesPath, providers, models, attachmentCapabilities);
39+
await writeRegistryFiles(
40+
distJsonPath,
41+
distTypesPath,
42+
providers,
43+
models,
44+
attachmentCapabilities,
45+
temperatureCapabilities,
46+
);
3247

3348
// Log summary
3449
console.info(`\nRegistered providers:`);
@@ -38,6 +53,9 @@ async function generateProviderRegistry(gateways: MastraModelGateway[]) {
3853
const capProviderCount = Object.keys(attachmentCapabilities).length;
3954
const capModelCount = Object.values(attachmentCapabilities).reduce((sum, models) => sum + models.length, 0);
4055
console.info(`\nAttachment-capable: ${capModelCount} models across ${capProviderCount} providers`);
56+
const tempProviderCount = Object.keys(temperatureCapabilities).length;
57+
const tempModelCount = Object.values(temperatureCapabilities).reduce((sum, models) => sum + models.length, 0);
58+
console.info(`Temperature-capable: ${tempModelCount} models across ${tempProviderCount} providers`);
4159
}
4260

4361
// Main execution

packages/core/src/llm/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export {
8383
parseModelString,
8484
getProviderConfig,
8585
modelSupportsAttachments,
86+
modelSupportsTemperature,
8687
} from './model/provider-registry.js';
8788
export type {
8889
ModelRouterModelId,

packages/core/src/llm/model/capabilities/anthropic.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,21 @@
1717
"claude-sonnet-4-5",
1818
"claude-sonnet-4-5-20250929",
1919
"claude-sonnet-4-6"
20+
],
21+
"temperature": [
22+
"claude-haiku-4-5",
23+
"claude-haiku-4-5-20251001",
24+
"claude-opus-4-0",
25+
"claude-opus-4-1",
26+
"claude-opus-4-1-20250805",
27+
"claude-opus-4-20250514",
28+
"claude-opus-4-5",
29+
"claude-opus-4-5-20251101",
30+
"claude-opus-4-6",
31+
"claude-sonnet-4-0",
32+
"claude-sonnet-4-20250514",
33+
"claude-sonnet-4-5",
34+
"claude-sonnet-4-5-20250929",
35+
"claude-sonnet-4-6"
2036
]
2137
}

packages/core/src/llm/model/capabilities/openai.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,20 @@
4545
"o3-pro",
4646
"o4-mini",
4747
"o4-mini-deep-research"
48+
],
49+
"temperature": [
50+
"gpt-3.5-turbo",
51+
"gpt-4",
52+
"gpt-4-turbo",
53+
"gpt-4.1",
54+
"gpt-4.1-mini",
55+
"gpt-4.1-nano",
56+
"gpt-4o",
57+
"gpt-4o-2024-05-13",
58+
"gpt-4o-2024-08-06",
59+
"gpt-4o-2024-11-20",
60+
"gpt-4o-mini",
61+
"gpt-5-chat-latest",
62+
"gpt-5.3-chat-latest"
4863
]
4964
}

packages/core/src/llm/model/gateways/base.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ export interface ProviderConfig {
2222

2323
/**
2424
* Compact capability data collected from gateways during generation.
25-
* Each provider maps to a list of model IDs that support attachments.
25+
* Each provider maps to a list of model IDs that support a capability.
2626
*/
2727
export type AttachmentCapabilities = Record<string, string[]>;
28+
export type TemperatureCapabilities = Record<string, string[]>;
2829

2930
/**
3031
* Union type for language models that can be returned by gateways.

packages/core/src/llm/model/gateways/models-dev.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { createGateway } from '@internal/ai-v6';
1515
import { createOpenRouter } from '@openrouter/ai-sdk-provider-v5';
1616
import { parseModelRouterId } from '../gateway-resolver.js';
1717
import { MastraModelGateway } from './base.js';
18-
import type { AttachmentCapabilities, GatewayLanguageModel, ProviderConfig } from './base.js';
18+
import type { AttachmentCapabilities, GatewayLanguageModel, ProviderConfig, TemperatureCapabilities } from './base.js';
1919
import { EXCLUDED_PROVIDERS, MASTRA_USER_AGENT, PROVIDERS_WITH_INSTALLED_PACKAGES } from './constants.js';
2020

2121
interface ModelsDevModelInfo {
@@ -24,6 +24,7 @@ interface ModelsDevModelInfo {
2424
status?: string;
2525
modalities?: { input?: string[]; output?: string[] };
2626
attachment?: boolean;
27+
temperature?: boolean;
2728
[key: string]: unknown;
2829
}
2930

@@ -110,6 +111,7 @@ export class ModelsDevGateway extends MastraModelGateway {
110111

111112
private providerConfigs: Record<string, ProviderConfig> = {};
112113
private attachmentCapabilities: AttachmentCapabilities = {};
114+
private temperatureCapabilities: TemperatureCapabilities = {};
113115

114116
constructor(providerConfigs?: Record<string, ProviderConfig>) {
115117
super();
@@ -124,6 +126,10 @@ export class ModelsDevGateway extends MastraModelGateway {
124126

125127
const data = (await response.json()) as ModelsDevResponse;
126128

129+
// Reset capability maps so removed providers/models are not retained across syncs
130+
this.attachmentCapabilities = {};
131+
this.temperatureCapabilities = {};
132+
127133
const providerConfigs: Record<string, ProviderConfig> = {};
128134

129135
for (const [providerId, providerInfo] of Object.entries(data)) {
@@ -161,6 +167,12 @@ export class ModelsDevGateway extends MastraModelGateway {
161167
.map(([modelId]) => modelId)
162168
.sort();
163169

170+
// Collect model IDs that support temperature sampling
171+
const temperatureModels = activeModels
172+
.filter(([, modelInfo]) => modelInfo?.temperature === true)
173+
.map(([modelId]) => modelId)
174+
.sort();
175+
164176
// Get the API URL - overrides take priority over models.dev data
165177
const url = PROVIDER_OVERRIDES[normalizedId]?.url || providerInfo.api;
166178

@@ -203,6 +215,9 @@ export class ModelsDevGateway extends MastraModelGateway {
203215
if (attachmentModels.length > 0) {
204216
this.attachmentCapabilities[normalizedId] = attachmentModels;
205217
}
218+
if (temperatureModels.length > 0) {
219+
this.temperatureCapabilities[normalizedId] = temperatureModels;
220+
}
206221
}
207222
}
208223

@@ -220,6 +235,10 @@ export class ModelsDevGateway extends MastraModelGateway {
220235
return this.attachmentCapabilities;
221236
}
222237

238+
getTemperatureCapabilities(): TemperatureCapabilities {
239+
return this.temperatureCapabilities;
240+
}
241+
223242
buildUrl(routerId: string, envVars?: typeof process.env): string | undefined {
224243
const { providerId } = parseModelRouterId(routerId);
225244

packages/core/src/llm/model/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export {
66
type ModelForProvider,
77
type AttachmentCapabilities,
88
modelSupportsAttachments,
9+
modelSupportsTemperature,
910
} from './provider-registry.js';
1011
export { resolveModelConfig, isOpenAICompatibleObjectConfig } from './resolve-model';
1112
export { resolveModelAuth, type ResolveModelAuthArgs } from './model-auth-resolver';

packages/core/src/llm/model/provider-registry.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ import type { ProviderConfig } from './gateways/base.js';
66
import { MastraGateway } from './gateways/mastra.js';
77
import { ModelsDevGateway } from './gateways/models-dev.js';
88
import { NetlifyGateway } from './gateways/netlify.js';
9-
import { GatewayRegistry, modelSupportsAttachments } from './provider-registry.js';
9+
import {
10+
GatewayRegistry,
11+
modelSupportsAttachments,
12+
modelSupportsTemperature,
13+
_resetCapabilityCaches,
14+
} from './provider-registry.js';
1015

1116
describe('modelSupportsAttachments', () => {
1217
afterEach(() => {
18+
_resetCapabilityCaches();
1319
vi.restoreAllMocks();
1420
});
1521

@@ -35,6 +41,35 @@ describe('modelSupportsAttachments', () => {
3541
});
3642
});
3743

44+
describe('modelSupportsTemperature', () => {
45+
beforeEach(() => {
46+
_resetCapabilityCaches();
47+
});
48+
afterEach(() => {
49+
vi.restoreAllMocks();
50+
});
51+
52+
it('returns true for models listed in the temperature capability', () => {
53+
expect(modelSupportsTemperature('openai/gpt-4o')).toBe(true);
54+
expect(modelSupportsTemperature('anthropic/claude-sonnet-4-6')).toBe(true);
55+
});
56+
57+
it('returns false for models whose provider is known but model is not in temperature list', () => {
58+
// gpt-5-pro is in the openai attachment list but NOT in the temperature list
59+
expect(modelSupportsTemperature('openai/gpt-5-pro')).toBe(false);
60+
});
61+
62+
it('returns undefined for unknown providers', () => {
63+
expect(modelSupportsTemperature('unknown-provider/some-model')).toBeUndefined();
64+
});
65+
66+
it('resolves nested provider model IDs through the fallback path', () => {
67+
// openrouter/anthropic/claude-sonnet-4-6 should resolve via the nested provider fallback
68+
expect(modelSupportsTemperature('openrouter/anthropic/claude-sonnet-4-6')).toBe(true);
69+
expect(modelSupportsTemperature('openrouter/openai/gpt-5-pro')).toBe(false);
70+
});
71+
});
72+
3873
describe('GatewayRegistry Auto-Refresh', () => {
3974
const CACHE_DIR = path.join(os.homedir(), '.cache', 'mastra');
4075
const CACHE_FILE = path.join(CACHE_DIR, 'gateway-refresh-time');

0 commit comments

Comments
 (0)