diff --git a/core/providers/openai/responses.go b/core/providers/openai/responses.go index 0f07167aaa2..3ab57be9658 100644 --- a/core/providers/openai/responses.go +++ b/core/providers/openai/responses.go @@ -342,7 +342,14 @@ func (resp *OpenAIResponsesRequest) filterUnsupportedTools() { // Filter tools to only include supported types filteredTools := make([]schemas.ResponsesTool, 0, len(resp.Tools)) for _, tool := range resp.Tools { - if supportedTypes[tool.Type] { + // OpenRouter exposes server-side tools under the "openrouter:" namespace + // (web_search, web_fetch, datetime, image_generation, apply_patch, subagent, ...). + // They are native to OpenRouter and must not be stripped by the + // OpenAI-oriented whitelist. Match the whole namespace so future tools are + // covered without per-tool additions. + isOpenRouterServerTool := resp.Provider == schemas.OpenRouter && + strings.HasPrefix(string(tool.Type), schemas.ResponsesToolTypeOpenRouterPrefix) + if supportedTypes[tool.Type] || isOpenRouterServerTool { // check for computer use preview if tool.Type == schemas.ResponsesToolTypeComputerUsePreview && tool.ResponsesToolComputerUsePreview != nil && tool.ResponsesToolComputerUsePreview.EnableZoom != nil { newTool := tool diff --git a/core/providers/openai/responses_test.go b/core/providers/openai/responses_test.go index 9dc241bcf5c..cc9aee8e072 100644 --- a/core/providers/openai/responses_test.go +++ b/core/providers/openai/responses_test.go @@ -1958,3 +1958,45 @@ func valuesEqual(v1, v2 interface{}) bool { return v1 == v2 } } + +func TestToOpenAIResponsesRequest_OpenRouterServerToolsPreserved(t *testing.T) { + makeReq := func(provider schemas.ModelProvider, toolType schemas.ResponsesToolType) *schemas.BifrostResponsesRequest { + return &schemas.BifrostResponsesRequest{ + Provider: provider, + Model: "anthropic/claude-haiku-4.5", + Input: []schemas.ResponsesMessage{ + { + Role: schemas.Ptr(schemas.ResponsesInputMessageRoleUser), + Content: &schemas.ResponsesMessageContent{ContentStr: schemas.Ptr("hi")}, + }, + }, + Params: &schemas.ResponsesParameters{ + Tools: []schemas.ResponsesTool{{Type: toolType}}, + }, + } + } + + // Any tool under the "openrouter:" namespace must survive for the OpenRouter + // provider (web_search, web_fetch, and any future server tool). + for _, toolType := range []schemas.ResponsesToolType{"openrouter:web_search", "openrouter:web_fetch"} { + t.Run("openrouter keeps "+string(toolType), func(t *testing.T) { + result := ToOpenAIResponsesRequest(nil, makeReq(schemas.OpenRouter, toolType)) + if result == nil { + t.Fatal("ToOpenAIResponsesRequest returned nil") + } + if len(result.Tools) != 1 || result.Tools[0].Type != toolType { + t.Fatalf("expected %s to be preserved for OpenRouter, got %+v", toolType, result.Tools) + } + }) + } + + t.Run("openai strips openrouter: namespace tools", func(t *testing.T) { + result := ToOpenAIResponsesRequest(nil, makeReq(schemas.OpenAI, "openrouter:web_search")) + if result == nil { + t.Fatal("ToOpenAIResponsesRequest returned nil") + } + if len(result.Tools) != 0 { + t.Fatalf("expected openrouter: tools to be stripped for OpenAI, got %+v", result.Tools) + } + }) +} diff --git a/core/schemas/responses.go b/core/schemas/responses.go index fb765d36e8d..50b4c1cdd77 100644 --- a/core/schemas/responses.go +++ b/core/schemas/responses.go @@ -1750,6 +1750,12 @@ const ( ResponsesToolTypeAdvisor ResponsesToolType = "advisor" ) +// ResponsesToolTypeOpenRouterPrefix is the namespace prefix for OpenRouter server +// tools (e.g. "openrouter:web_search", "openrouter:web_fetch", "openrouter:datetime", +// "openrouter:image_generation", "openrouter:apply_patch", "openrouter:subagent"). +// These are executed server-side by OpenRouter and are not part of the OpenAI spec. +const ResponsesToolTypeOpenRouterPrefix = "openrouter:" + // normalizeResponsesToolType maps versioned/provider-specific tool type strings // to their canonical ResponsesToolType. For example, "web_search_20250305" → "web_search". // Returns the input unchanged if it's already canonical or unrecognized.