Skip to content

Commit c2ea621

Browse files
committed
fix(models): derive forced tool support from capability metadata
1 parent c9f9ef2 commit c2ea621

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

@@ -75,11 +76,49 @@ describe('forced tool use capability', () => {
7576
'claude-fable-5-1',
7677
'CLAUDE-FABLE-5-1-20260901',
7778
'azure-anthropic/claude-fable-5-1',
79+
'azure-anthropic/claude-fable-5-1-20260901',
80+
'bedrock/anthropic.claude-fable-5-1-v1:0',
7881
'claude-mythos-5-1',
82+
'claude-mythos-5-1-20260901',
7983
])('disables forced tool use for %s', (model) => {
84+
expect(getModelCapabilities(model)).toMatchObject({
85+
toolUsageControl: true,
86+
forcedToolUse: false,
87+
})
8088
expect(supportsForcedToolUse(model)).toBe(false)
8189
})
8290

91+
it('adds only known alias capabilities to provider defaults', () => {
92+
expect(getModelCapabilities('claude-mythos-5-1')).toEqual({
93+
...PROVIDER_DEFINITIONS.anthropic.capabilities,
94+
forcedToolUse: false,
95+
})
96+
})
97+
98+
it('inherits alias capabilities for dynamic models and respects explicit overrides', () => {
99+
const originalModels = PROVIDER_DEFINITIONS.openrouter.models
100+
const modelId = 'anthropic/claude-fable-5-1'
101+
try {
102+
updateOpenRouterModels([modelId])
103+
expect(getModelCapabilities(modelId)?.forcedToolUse).toBe(false)
104+
expect(supportsForcedToolUse(modelId)).toBe(false)
105+
106+
PROVIDER_DEFINITIONS.openrouter.models[0].capabilities.forcedToolUse = true
107+
expect(getModelCapabilities(modelId)?.forcedToolUse).toBe(true)
108+
expect(supportsForcedToolUse(modelId)).toBe(true)
109+
} finally {
110+
PROVIDER_DEFINITIONS.openrouter.models = originalModels
111+
}
112+
})
113+
114+
it.each(['claude-fable-5-10', 'claude-mythos-5-10', 'claude-not-fable-5-1'])(
115+
'does not apply alias restrictions to unrelated model %s',
116+
(model) => {
117+
expect(getModelCapabilities(model)).toEqual(PROVIDER_DEFINITIONS.anthropic.capabilities)
118+
expect(supportsForcedToolUse(model)).toBe(true)
119+
}
120+
)
121+
83122
it.each(['claude-sonnet-5', 'claude-fable-5', 'claude-opus-5', 'gpt-5.5'])(
84123
'inherits provider tool-control support for %s',
85124
(model) => {
@@ -88,6 +127,7 @@ describe('forced tool use capability', () => {
88127
)
89128

90129
it('does not enable Force for an unknown model without tool-control capabilities', () => {
130+
expect(getModelCapabilities('unknown-model')).toBeNull()
91131
expect(supportsForcedToolUse('unknown-model')).toBe(false)
92132
})
93133
})

apps/sim/providers/models.ts

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

169+
const CLAUDE_5_1_TOOL_CAPABILITIES = { forcedToolUse: false } as const satisfies ModelCapabilities
170+
171+
/** Known capabilities for model aliases; explicit catalog capabilities take precedence. */
172+
const MODEL_CAPABILITY_FALLBACKS = [
173+
{
174+
pattern: /(?:^|[/.])claude-(?:fable|mythos)-5-1(?:$|[-:])/,
175+
capabilities: CLAUDE_5_1_TOOL_CAPABILITIES,
176+
},
177+
] satisfies { pattern: RegExp; capabilities: ModelCapabilities }[]
178+
169179
export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
170180
fireworks: {
171181
id: 'fireworks',
@@ -916,7 +926,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
916926
updatedAt: '2026-09-04',
917927
},
918928
capabilities: {
919-
forcedToolUse: false,
929+
...CLAUDE_5_1_TOOL_CAPABILITIES,
920930
nativeStructuredOutputs: true,
921931
maxOutputTokens: 128000,
922932
promptCaching: { minimumCacheableTokens: 512 },
@@ -4387,25 +4397,36 @@ export function getModelPricing(modelId: string): ModelPricing | null {
43874397
}
43884398

43894399
export function getModelCapabilities(modelId: string): ModelCapabilities | null {
4400+
const normalizedModel = modelId.toLowerCase()
4401+
const fallbackCapabilities = MODEL_CAPABILITY_FALLBACKS.find(({ pattern }) =>
4402+
pattern.test(normalizedModel)
4403+
)?.capabilities
4404+
43904405
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
4391-
const model = provider.models.find((m) => m.id.toLowerCase() === modelId.toLowerCase())
4406+
const model = provider.models.find((m) => m.id.toLowerCase() === normalizedModel)
43924407
if (model) {
4393-
const capabilities: ModelCapabilities = { ...provider.capabilities, ...model.capabilities }
4408+
const capabilities: ModelCapabilities = {
4409+
...provider.capabilities,
4410+
...fallbackCapabilities,
4411+
...model.capabilities,
4412+
}
43944413
return capabilities
43954414
}
43964415
}
43974416

43984417
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
43994418
if (provider.modelPatterns) {
44004419
for (const pattern of provider.modelPatterns) {
4401-
if (pattern.test(modelId.toLowerCase())) {
4402-
return provider.capabilities || null
4420+
if (pattern.test(normalizedModel)) {
4421+
return fallbackCapabilities
4422+
? { ...provider.capabilities, ...fallbackCapabilities }
4423+
: provider.capabilities || null
44034424
}
44044425
}
44054426
}
44064427
}
44074428

4408-
return null
4429+
return fallbackCapabilities || null
44094430
}
44104431

44114432
export function getModelsWithTemperatureSupport(): string[] {
@@ -4484,18 +4505,10 @@ export function supportsToolUsageControl(providerId: string): boolean {
44844505
return getProvidersWithToolUsageControl().includes(providerId)
44854506
}
44864507

4487-
/** Whether the model accepts forced tool choice, including uncataloged Claude aliases. */
4508+
/** Whether the model accepts forced tool choice. */
44884509
export function supportsForcedToolUse(modelId: string): boolean {
44894510
const capabilities = getModelCapabilities(modelId)
4490-
if (capabilities?.forcedToolUse !== undefined) return capabilities.forcedToolUse
4491-
4492-
/** Preserve the restriction for date-suffixed and reseller IDs outside the catalog. */
4493-
const normalizedModel = modelId.toLowerCase()
4494-
if (normalizedModel.includes('fable-5-1') || normalizedModel.includes('mythos-5-1')) {
4495-
return false
4496-
}
4497-
4498-
return capabilities?.toolUsageControl ?? false
4511+
return capabilities?.forcedToolUse ?? capabilities?.toolUsageControl ?? false
44994512
}
45004513

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

0 commit comments

Comments
 (0)