Skip to content

Commit 5a8e54b

Browse files
committed
fix(models): derive forced tool support from capability metadata
1 parent 15bf22b commit 5a8e54b

2 files changed

Lines changed: 69 additions & 16 deletions

File tree

apps/sim/providers/models.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
PROVIDER_DEFINITIONS,
1717
supportsForcedToolUse,
1818
updateFireworksModels,
19+
updateOpenRouterModels,
1920
} from '@/providers/models'
2021
import { supportsPromptCaching } from '@/providers/utils'
2122

@@ -31,11 +32,49 @@ describe('forced tool use capability', () => {
3132
'claude-fable-5-1',
3233
'CLAUDE-FABLE-5-1-20260901',
3334
'azure-anthropic/claude-fable-5-1',
35+
'azure-anthropic/claude-fable-5-1-20260901',
36+
'bedrock/anthropic.claude-fable-5-1-v1:0',
3437
'claude-mythos-5-1',
38+
'claude-mythos-5-1-20260901',
3539
])('disables forced tool use for %s', (model) => {
40+
expect(getModelCapabilities(model)).toMatchObject({
41+
toolUsageControl: true,
42+
forcedToolUse: false,
43+
})
3644
expect(supportsForcedToolUse(model)).toBe(false)
3745
})
3846

47+
it('adds only known alias capabilities to provider defaults', () => {
48+
expect(getModelCapabilities('claude-mythos-5-1')).toEqual({
49+
...PROVIDER_DEFINITIONS.anthropic.capabilities,
50+
forcedToolUse: false,
51+
})
52+
})
53+
54+
it('inherits alias capabilities for dynamic models and respects explicit overrides', () => {
55+
const originalModels = PROVIDER_DEFINITIONS.openrouter.models
56+
const modelId = 'anthropic/claude-fable-5-1'
57+
try {
58+
updateOpenRouterModels([modelId])
59+
expect(getModelCapabilities(modelId)?.forcedToolUse).toBe(false)
60+
expect(supportsForcedToolUse(modelId)).toBe(false)
61+
62+
PROVIDER_DEFINITIONS.openrouter.models[0].capabilities.forcedToolUse = true
63+
expect(getModelCapabilities(modelId)?.forcedToolUse).toBe(true)
64+
expect(supportsForcedToolUse(modelId)).toBe(true)
65+
} finally {
66+
PROVIDER_DEFINITIONS.openrouter.models = originalModels
67+
}
68+
})
69+
70+
it.each(['claude-fable-5-10', 'claude-mythos-5-10', 'claude-not-fable-5-1'])(
71+
'does not apply alias restrictions to unrelated model %s',
72+
(model) => {
73+
expect(getModelCapabilities(model)).toEqual(PROVIDER_DEFINITIONS.anthropic.capabilities)
74+
expect(supportsForcedToolUse(model)).toBe(true)
75+
}
76+
)
77+
3978
it.each(['claude-sonnet-5', 'claude-fable-5', 'claude-opus-5', 'gpt-5.5'])(
4079
'inherits provider tool-control support for %s',
4180
(model) => {
@@ -44,6 +83,7 @@ describe('forced tool use capability', () => {
4483
)
4584

4685
it('does not enable Force for an unknown model without tool-control capabilities', () => {
86+
expect(getModelCapabilities('unknown-model')).toBeNull()
4787
expect(supportsForcedToolUse('unknown-model')).toBe(false)
4888
})
4989
})

apps/sim/providers/models.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,16 @@ export function getProviderFileAttachment(providerId: string): ProviderFileAttac
164164
return PROVIDER_DEFINITIONS[providerId]?.fileAttachment ?? DEFAULT_FILE_ATTACHMENT
165165
}
166166

167+
const CLAUDE_5_1_TOOL_CAPABILITIES = { forcedToolUse: false } as const satisfies ModelCapabilities
168+
169+
/** Known capabilities for model aliases; explicit catalog capabilities take precedence. */
170+
const MODEL_CAPABILITY_FALLBACKS = [
171+
{
172+
pattern: /(?:^|[/.])claude-(?:fable|mythos)-5-1(?:$|[-:])/,
173+
capabilities: CLAUDE_5_1_TOOL_CAPABILITIES,
174+
},
175+
] satisfies { pattern: RegExp; capabilities: ModelCapabilities }[]
176+
167177
export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
168178
fireworks: {
169179
id: 'fireworks',
@@ -836,7 +846,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
836846
updatedAt: '2026-09-01',
837847
},
838848
capabilities: {
839-
forcedToolUse: false,
849+
...CLAUDE_5_1_TOOL_CAPABILITIES,
840850
nativeStructuredOutputs: true,
841851
maxOutputTokens: 128000,
842852
promptCaching: { minimumCacheableTokens: 512 },
@@ -4297,25 +4307,36 @@ export function getModelPricing(modelId: string): ModelPricing | null {
42974307
}
42984308

42994309
export function getModelCapabilities(modelId: string): ModelCapabilities | null {
4310+
const normalizedModel = modelId.toLowerCase()
4311+
const fallbackCapabilities = MODEL_CAPABILITY_FALLBACKS.find(({ pattern }) =>
4312+
pattern.test(normalizedModel)
4313+
)?.capabilities
4314+
43004315
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
4301-
const model = provider.models.find((m) => m.id.toLowerCase() === modelId.toLowerCase())
4316+
const model = provider.models.find((m) => m.id.toLowerCase() === normalizedModel)
43024317
if (model) {
4303-
const capabilities: ModelCapabilities = { ...provider.capabilities, ...model.capabilities }
4318+
const capabilities: ModelCapabilities = {
4319+
...provider.capabilities,
4320+
...fallbackCapabilities,
4321+
...model.capabilities,
4322+
}
43044323
return capabilities
43054324
}
43064325
}
43074326

43084327
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
43094328
if (provider.modelPatterns) {
43104329
for (const pattern of provider.modelPatterns) {
4311-
if (pattern.test(modelId.toLowerCase())) {
4312-
return provider.capabilities || null
4330+
if (pattern.test(normalizedModel)) {
4331+
return fallbackCapabilities
4332+
? { ...provider.capabilities, ...fallbackCapabilities }
4333+
: provider.capabilities || null
43134334
}
43144335
}
43154336
}
43164337
}
43174338

4318-
return null
4339+
return fallbackCapabilities || null
43194340
}
43204341

43214342
export function getModelsWithTemperatureSupport(): string[] {
@@ -4394,18 +4415,10 @@ export function supportsToolUsageControl(providerId: string): boolean {
43944415
return getProvidersWithToolUsageControl().includes(providerId)
43954416
}
43964417

4397-
/** Whether the model accepts forced tool choice, including uncataloged Claude aliases. */
4418+
/** Whether the model accepts forced tool choice. */
43984419
export function supportsForcedToolUse(modelId: string): boolean {
43994420
const capabilities = getModelCapabilities(modelId)
4400-
if (capabilities?.forcedToolUse !== undefined) return capabilities.forcedToolUse
4401-
4402-
/** Preserve the restriction for date-suffixed and reseller IDs outside the catalog. */
4403-
const normalizedModel = modelId.toLowerCase()
4404-
if (normalizedModel.includes('fable-5-1') || normalizedModel.includes('mythos-5-1')) {
4405-
return false
4406-
}
4407-
4408-
return capabilities?.toolUsageControl ?? false
4421+
return capabilities?.forcedToolUse ?? capabilities?.toolUsageControl ?? false
44094422
}
44104423

44114424
export function updateOllamaModels(models: string[]): void {

0 commit comments

Comments
 (0)