Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions bifrost/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -1585,11 +1585,25 @@ func (b *LLM) providerSupportsNativeTools() bool {
return supportsNativeToolsProvider(b.provider)
}

func (b *LLM) providerCanAutoUseResponsesAPI() bool {
switch b.provider {
case schemas.OpenAI, schemas.Azure:
// Direct OpenAI always sets useResponsesAPI. OpenAI-compatible and Azure
// services must not have native tools silently override their service toggle.
return b.useResponsesAPI
default:
return true
}
}

// shouldUseResponsesAPI determines if the Responses API should be used for this request.
func (b *LLM) shouldUseResponsesAPI(cfg llm.LanguageModelConfig) bool {
if b.useResponsesAPI {
return true
}
if !b.providerCanAutoUseResponsesAPI() {
return false
}
if b.providerSupportsNativeTools() && len(b.enabledNativeTools) > 0 {
return true
}
Expand Down
16 changes: 12 additions & 4 deletions bifrost/bifrost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,21 +538,29 @@ func TestShouldUseResponsesAPI(t *testing.T) {
expected bool
}{
{
name: "native tools configured returns true",
name: "OpenAI native tools require Responses API service flag",
provider: schemas.OpenAI,
enabledNativeTools: []string{"web_search"},
expected: false,
},
{
name: "OpenAI native tools with Responses API service flag returns true",
provider: schemas.OpenAI,
useResponsesAPI: true,
enabledNativeTools: []string{"web_search"},
expected: true,
},
{
name: "NativeWebSearchAllowed with web_search enabled returns true",
name: "OpenAI NativeWebSearchAllowed requires Responses API service flag",
provider: schemas.OpenAI,
enabledNativeTools: []string{"web_search"},
cfg: llm.LanguageModelConfig{NativeWebSearchAllowed: true},
expected: true,
expected: false,
},
{
name: "NativeWebSearchAllowed without web_search in tools returns true",
name: "OpenAI NativeWebSearchAllowed with Responses API service flag returns true",
provider: schemas.OpenAI,
useResponsesAPI: true,
enabledNativeTools: nil,
cfg: llm.LanguageModelConfig{NativeWebSearchAllowed: true},
expected: true,
Expand Down
23 changes: 22 additions & 1 deletion bifrost/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ func filterNativeToolsForServiceType(serviceType string, tools []string) []strin
return filtered
}

func serviceAllowsNativeTools(svc llm.ServiceConfig) bool {
switch svc.Type {
case llm.ServiceTypeOpenAI, llm.ServiceTypeAnthropic, llm.ServiceTypeGemini, llm.ServiceTypeVertex:
return true
case llm.ServiceTypeOpenAICompatible, llm.ServiceTypeAzure:
return llm.ServiceUsesResponsesAPI(svc)
default:
return false
}
}

func nativeToolsForService(svc llm.ServiceConfig, tools []string) []string {
if len(tools) == 0 {
return tools
}
if !serviceAllowsNativeTools(svc) {
return []string{}
}
return filterNativeToolsForServiceType(svc.Type, tools)
}

// NewFromServiceConfig creates a LLM instance from ServiceConfig and BotConfig.
// fallbackServices is an ordered slice of fallback services resolved from the
// primary service's fallback chain (see llm.ResolveFallbackChain). Each fallback
Expand Down Expand Up @@ -105,7 +126,7 @@ func NewFromServiceConfig(serviceConfig llm.ServiceConfig, botConfig llm.BotConf
UseResponsesAPI: llm.ServiceUsesResponsesAPI(serviceConfig),

// Bot-specific configuration
EnabledNativeTools: filterNativeToolsForServiceType(serviceConfig.Type, botConfig.EnabledNativeTools),
EnabledNativeTools: nativeToolsForService(serviceConfig, botConfig.EnabledNativeTools),
ReasoningEnabled: botConfig.ReasoningEnabled,
ReasoningEffort: botConfig.ReasoningEffort,
ThinkingBudget: botConfig.ThinkingBudget,
Expand Down
66 changes: 66 additions & 0 deletions bifrost/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,53 @@ func TestFilterNativeToolsForServiceType(t *testing.T) {
}
}

func TestNativeToolsForService(t *testing.T) {
tools := []string{"web_search"}

tests := []struct {
name string
svc llm.ServiceConfig
want []string
}{
{
name: "OpenAI compatible without Responses API drops stale native tools",
svc: llm.ServiceConfig{Type: llm.ServiceTypeOpenAICompatible, UseResponsesAPI: false},
want: []string{},
},
{
name: "OpenAI compatible with Responses API keeps native tools",
svc: llm.ServiceConfig{Type: llm.ServiceTypeOpenAICompatible, UseResponsesAPI: true},
want: tools,
},
{
name: "Azure without Responses API drops stale native tools",
svc: llm.ServiceConfig{Type: llm.ServiceTypeAzure, UseResponsesAPI: false},
want: []string{},
},
{
name: "OpenAI direct keeps native tools",
svc: llm.ServiceConfig{Type: llm.ServiceTypeOpenAI, UseResponsesAPI: false},
want: tools,
},
{
name: "Anthropic keeps native tools",
svc: llm.ServiceConfig{Type: llm.ServiceTypeAnthropic, UseResponsesAPI: false},
want: tools,
},
{
name: "unsupported service drops native tools",
svc: llm.ServiceConfig{Type: llm.ServiceTypeCohere, UseResponsesAPI: true},
want: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := nativeToolsForService(tt.svc, tools)
assert.Equal(t, tt.want, got)
})
}
}

func TestNewFromServiceConfigOpenAIForcesResponsesAPI(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -97,6 +144,25 @@ func TestNewFromServiceConfigOpenAIForcesResponsesAPI(t *testing.T) {
}
}

func TestNewFromServiceConfigFiltersNativeToolsWhenResponsesAPIDisabled(t *testing.T) {
service := llm.ServiceConfig{
ID: "test",
Type: llm.ServiceTypeOpenAICompatible,
APIKey: "key",
APIURL: "http://localhost",
UseResponsesAPI: false,
}
bot := llm.BotConfig{
EnabledNativeTools: []string{"web_search"},
}

llmInstance, err := NewFromServiceConfig(service, bot, nil)
require.NoError(t, err)
defer llmInstance.Shutdown()

assert.Empty(t, llmInstance.enabledNativeTools)
}

// TestNewFromServiceConfigPropagatesInputTokenLimit pins the contract that a
// manually-set "Input token limit" in the system console flows through to
// the running LLM, so the context indicator can compute utilization. A user
Expand Down
3 changes: 3 additions & 0 deletions bots/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ func (b *Bot) HasNativeWebSearchEnabled() bool {
if !bifrost.SupportsNativeTools(b.service.Type) {
return false
}
if !llm.ServiceUsesResponsesAPI(b.service) && (b.service.Type == llm.ServiceTypeOpenAICompatible || b.service.Type == llm.ServiceTypeAzure) {
return false
}
for _, tool := range b.cfg.EnabledNativeTools {
if tool == "web_search" {
return true
Expand Down
22 changes: 22 additions & 0 deletions bots/bots_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1079,3 +1079,25 @@ func TestHasNativeWebSearchEnabledSupportedServiceType(t *testing.T) {
)
require.True(t, b.HasNativeWebSearchEnabled())
}

func TestHasNativeWebSearchEnabledOpenAICompatibleRequiresResponsesAPI(t *testing.T) {
tests := []struct {
name string
useResponsesAPI bool
want bool
}{
{name: "disabled", useResponsesAPI: false, want: false},
{name: "enabled", useResponsesAPI: true, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := NewBot(
llm.BotConfig{EnabledNativeTools: []string{"web_search"}},
llm.ServiceConfig{Type: llm.ServiceTypeOpenAICompatible, UseResponsesAPI: tt.useResponsesAPI},
&model.Bot{UserId: "b1"},
nil,
)
require.Equal(t, tt.want, b.HasNativeWebSearchEnabled())
})
}
}
26 changes: 25 additions & 1 deletion webapp/src/components/agents/tabs/config_tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ type Props = {
const visionToolServiceTypes = ['openai', 'openaicompatible', 'azure', 'anthropic', 'cohere', 'mistral', 'gemini', 'vertex'];
const openAIStructuredOutputServiceTypes = ['openai', 'openaicompatible', 'azure'];

function serviceSupportsNativeTools(service?: Pick<ServiceInfo, 'type' | 'useResponsesAPI'>): boolean {
if (!service) {
return false;
}
if (service.type === 'openai' || service.type === 'anthropic' || service.type === 'gemini' || service.type === 'vertex') {
return true;
}
if (service.type === 'openaicompatible' || service.type === 'azure') {
return service.useResponsesAPI;
}
return false;
}

function defaultNativeToolsForService(service?: Pick<ServiceInfo, 'type' | 'useResponsesAPI'>): string[] {
return serviceSupportsNativeTools(service) ? ['web_search'] : [];
}

const ConfigTab = (props: Props) => {
const {draft, onChange, onAvatarChange, services, errors = {}, usernameLocked = false} = props;
const intl = useIntl();
Expand Down Expand Up @@ -73,7 +90,7 @@ const ConfigTab = (props: Props) => {
...(sameServiceType ?
{} :
{
enabledNativeTools: ['web_search'],
enabledNativeTools: defaultNativeToolsForService(nextSvc),
reasoningEnabled: true,
reasoningEffort: 'medium',
thinkingBudget: 0,
Expand All @@ -85,6 +102,13 @@ const ConfigTab = (props: Props) => {
}, [draft.serviceId, onChange, services]);

const selectedService = services.find((s) => s.id === draft.serviceId);
const supportsNativeTools = serviceSupportsNativeTools(selectedService);

useEffect(() => {
if (selectedService && !supportsNativeTools && draft.enabledNativeTools.length > 0) {
onChange({enabledNativeTools: []});
}
}, [draft.enabledNativeTools.length, onChange, selectedService, supportsNativeTools]);

const supportsModelFetching = Boolean(selectedService &&
(selectedService.type === 'anthropic' ||
Expand Down
21 changes: 21 additions & 0 deletions webapp/src/components/system_console/bot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ type ModelInfo = {
displayName: string
}

function serviceSupportsNativeTools(service?: LLMService): boolean {
if (!service) {
return false;
}
if (service.type === 'openai' || service.type === 'anthropic' || service.type === 'gemini' || service.type === 'vertex') {
return true;
}
if (service.type === 'openaicompatible' || service.type === 'azure') {
return service.useResponsesAPI;
}
return false;
}

const Bot = (props: Props) => {
const [open, setOpen] = useState(false);
const intl = useIntl();
Expand All @@ -161,6 +174,8 @@ const Bot = (props: Props) => {

// Find the selected service
const selectedService = props.services.find((s) => s.id === props.bot.serviceID);
const supportsNativeTools = serviceSupportsNativeTools(selectedService);
const enabledNativeToolsLength = (props.bot.enabledNativeTools || []).length;
const supportsModelFetching = selectedService &&
(selectedService.type === 'anthropic' ||
selectedService.type === 'openai' ||
Expand All @@ -169,6 +184,12 @@ const Bot = (props: Props) => {
selectedService.type === 'gemini' ||
selectedService.type === 'vertex');

useEffect(() => {
if (selectedService && !supportsNativeTools && enabledNativeToolsLength > 0) {
props.onChange({...props.bot, enabledNativeTools: []});
}
}, [enabledNativeToolsLength, props.bot, props.onChange, selectedService, supportsNativeTools]);

// Fetch models when the service changes
useEffect(() => {
if (!supportsModelFetching || !selectedService) {
Expand Down