Skip to content
Merged
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
9 changes: 8 additions & 1 deletion core/providers/openai/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions core/providers/openai/responses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
6 changes: 6 additions & 0 deletions core/schemas/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down