mirror of
https://github.com/danielmiessler/fabric.git
synced 2026-09-10 07:36:44 -04:00
feat(openai): add GrokAI search grounding via xAI Responses API
xAI's Responses API accepts web_search and x_search tool types, but fabric hardcoded OpenAI's web_search_preview tool name in buildResponseParams, causing GrokAI plus --search to fail with HTTP 422. This adds two new fields to openai_compatible.ProviderConfig: - WebSearchToolName: override the default web_search tool name string - EnableXSearch: append xAI's x_search tool when search is enabled Both fields are empty/false by default, preserving backwards compatibility for all existing providers. GrokAI now sets WebSearchToolName to "web_search" and EnableXSearch to true. Tests added in openai_test.go cover the new override paths and confirm the default provider behavior is unchanged. Verified with live xAI API key: fabric -V GrokAI --search "query" now returns grounded results with real source URLs.
This commit is contained in:
parent
42311fe04b
commit
7ced82f782
|
|
@ -88,7 +88,7 @@ type Flags struct {
|
|||
ListStrategies bool `long:"liststrategies" description:"List all strategies"`
|
||||
ListVendors bool `long:"listvendors" description:"List all vendors"`
|
||||
ShellCompleteOutput bool `long:"shell-complete-list" description:"Output raw list without headers/formatting (for shell completion)"`
|
||||
Search bool `long:"search" description:"Enable web search tool for supported models (Anthropic, OpenAI, Gemini)"`
|
||||
Search bool `long:"search" description:"Enable web search tool for supported models (Anthropic, OpenAI, Gemini, Grok)"`
|
||||
SearchLocation string `long:"search-location" description:"Set location for web search results (e.g., 'America/Los_Angeles')"`
|
||||
ImageFile string `long:"image-file" description:"Save generated image to specified file path (e.g., 'output.png')"`
|
||||
ImageSize string `long:"image-size" description:"Image dimensions: 1024x1024, 1536x1024, 1024x1536, auto (default: auto)"`
|
||||
|
|
|
|||
|
|
@ -66,6 +66,15 @@ type Client struct {
|
|||
ApiClient *openai.Client
|
||||
ImplementsResponses bool // Whether this provider supports the Responses API
|
||||
httpClient *http.Client
|
||||
// webSearchToolName, when non-empty, overrides the default
|
||||
// "web_search_preview" tool name emitted on the Responses API.
|
||||
// Used by OpenAI-compatible providers whose upstream API expects a
|
||||
// different tool type string (xAI expects "web_search").
|
||||
webSearchToolName string
|
||||
// enableXSearch, when true, appends an additional "x_search" tool
|
||||
// entry alongside the web search tool when Search is enabled.
|
||||
// This is an xAI-specific live search grounding tool.
|
||||
enableXSearch bool
|
||||
}
|
||||
|
||||
// SetResponsesAPIEnabled configures whether to use the Responses API
|
||||
|
|
@ -73,6 +82,21 @@ func (o *Client) SetResponsesAPIEnabled(enabled bool) {
|
|||
o.ImplementsResponses = enabled
|
||||
}
|
||||
|
||||
// SetWebSearchToolName overrides the default "web_search_preview" tool
|
||||
// name emitted on the Responses API when Search is enabled. Pass an empty
|
||||
// string to keep the OpenAI default. Non-OpenAI providers (for example,
|
||||
// xAI) may require "web_search" instead.
|
||||
func (o *Client) SetWebSearchToolName(name string) {
|
||||
o.webSearchToolName = name
|
||||
}
|
||||
|
||||
// SetEnableXSearch toggles whether an additional xAI "x_search" tool
|
||||
// entry is appended when Search is enabled. Non-xAI providers should
|
||||
// leave this false.
|
||||
func (o *Client) SetEnableXSearch(enabled bool) {
|
||||
o.enableXSearch = enabled
|
||||
}
|
||||
|
||||
// checkImageGenerationCompatibility warns if the model doesn't support image generation
|
||||
func checkImageGenerationCompatibility(model string) {
|
||||
if !supportsImageGeneration(model) {
|
||||
|
|
@ -253,11 +277,18 @@ func (o *Client) buildResponseParams(
|
|||
// Add tools if enabled
|
||||
var tools []responses.ToolUnionParam
|
||||
|
||||
// Add web search tool if enabled
|
||||
// Add web search tool if enabled. The default tool name is OpenAI's
|
||||
// "web_search_preview", but providers may override it (for example,
|
||||
// xAI's Responses API requires "web_search").
|
||||
if opts.Search {
|
||||
webSearchTool := responses.ToolParamOfWebSearchPreview("web_search_preview")
|
||||
searchToolName := responses.WebSearchToolType("web_search_preview")
|
||||
if o.webSearchToolName != "" {
|
||||
searchToolName = responses.WebSearchToolType(o.webSearchToolName)
|
||||
}
|
||||
webSearchTool := responses.ToolParamOfWebSearchPreview(searchToolName)
|
||||
|
||||
// Add user location if provided
|
||||
// Add user location if provided. Only attach when the caller
|
||||
// asked for it; xAI rejects unexpected location payloads.
|
||||
if opts.SearchLocation != "" {
|
||||
webSearchTool.OfWebSearchPreview.UserLocation = responses.WebSearchToolUserLocationParam{
|
||||
Type: "approximate",
|
||||
|
|
@ -266,6 +297,20 @@ func (o *Client) buildResponseParams(
|
|||
}
|
||||
|
||||
tools = append(tools, webSearchTool)
|
||||
|
||||
// Append xAI's live "x_search" tool when the provider opts in.
|
||||
// The xAI Responses API accepts a bare {"type":"x_search"}
|
||||
// entry with no other required fields. We reuse the SDK's
|
||||
// WebSearchToolParam as a minimal container since every other
|
||||
// field is omitzero and will be elided during JSON marshalling.
|
||||
if o.enableXSearch {
|
||||
xSearchTool := responses.ToolUnionParam{
|
||||
OfWebSearchPreview: &responses.WebSearchToolParam{
|
||||
Type: responses.WebSearchToolType("x_search"),
|
||||
},
|
||||
}
|
||||
tools = append(tools, xSearchTool)
|
||||
}
|
||||
}
|
||||
|
||||
// Add image generation tool if needed
|
||||
|
|
|
|||
|
|
@ -129,6 +129,88 @@ func TestBuildResponseParams_WithSearchAndLocation(t *testing.T) {
|
|||
assert.Equal(t, opts.SearchLocation, userLocation.Timezone.Value)
|
||||
}
|
||||
|
||||
// TestBuildResponseParams_GrokAI_WithSearch verifies that a client
|
||||
// configured with a custom web search tool name and x_search enabled
|
||||
// emits both tool entries with the xAI-expected type strings.
|
||||
func TestBuildResponseParams_GrokAI_WithSearch(t *testing.T) {
|
||||
client := NewClient()
|
||||
client.SetWebSearchToolName("web_search")
|
||||
client.SetEnableXSearch(true)
|
||||
|
||||
opts := &domain.ChatOptions{
|
||||
Model: "grok-4-fast-reasoning",
|
||||
Temperature: 0.7,
|
||||
Search: true,
|
||||
}
|
||||
|
||||
msgs := []*chat.ChatCompletionMessage{
|
||||
{Role: "user", Content: "What happened in the news today?"},
|
||||
}
|
||||
|
||||
params := client.buildResponseParams(msgs, opts)
|
||||
|
||||
assert.NotNil(t, params.Tools, "Expected tools when search is enabled")
|
||||
assert.Len(t, params.Tools, 2, "Expected web_search plus x_search tools")
|
||||
|
||||
webSearchTool := params.Tools[0]
|
||||
assert.NotNil(t, webSearchTool.OfWebSearchPreview, "Expected web search tool slot")
|
||||
assert.Equal(t, responses.WebSearchToolType("web_search"), webSearchTool.OfWebSearchPreview.Type)
|
||||
|
||||
xSearchTool := params.Tools[1]
|
||||
assert.NotNil(t, xSearchTool.OfWebSearchPreview, "Expected x_search tool slot")
|
||||
assert.Equal(t, responses.WebSearchToolType("x_search"), xSearchTool.OfWebSearchPreview.Type)
|
||||
}
|
||||
|
||||
// TestBuildResponseParams_DefaultProvider_Unchanged guards backwards
|
||||
// compatibility. A client that does not set the new override fields
|
||||
// must continue emitting a single web_search_preview tool entry.
|
||||
func TestBuildResponseParams_DefaultProvider_Unchanged(t *testing.T) {
|
||||
client := NewClient()
|
||||
|
||||
opts := &domain.ChatOptions{
|
||||
Model: "gpt-4o",
|
||||
Temperature: 0.7,
|
||||
Search: true,
|
||||
}
|
||||
|
||||
msgs := []*chat.ChatCompletionMessage{
|
||||
{Role: "user", Content: "What is the capital of France?"},
|
||||
}
|
||||
|
||||
params := client.buildResponseParams(msgs, opts)
|
||||
|
||||
assert.NotNil(t, params.Tools, "Expected tools when search is enabled")
|
||||
assert.Len(t, params.Tools, 1, "Expected exactly one tool for default provider")
|
||||
|
||||
tool := params.Tools[0]
|
||||
assert.NotNil(t, tool.OfWebSearchPreview, "Expected web search tool slot")
|
||||
assert.Equal(t, responses.WebSearchToolType("web_search_preview"), tool.OfWebSearchPreview.Type)
|
||||
}
|
||||
|
||||
// TestBuildResponseParams_GrokAI_WithoutSearch confirms that a GrokAI
|
||||
// style client without Search enabled does not append any tools.
|
||||
// This protects the no-search path from regressions introduced by the
|
||||
// new override logic.
|
||||
func TestBuildResponseParams_GrokAI_WithoutSearch(t *testing.T) {
|
||||
client := NewClient()
|
||||
client.SetWebSearchToolName("web_search")
|
||||
client.SetEnableXSearch(true)
|
||||
|
||||
opts := &domain.ChatOptions{
|
||||
Model: "grok-4-fast-reasoning",
|
||||
Temperature: 0.7,
|
||||
Search: false,
|
||||
}
|
||||
|
||||
msgs := []*chat.ChatCompletionMessage{
|
||||
{Role: "user", Content: "Hello"},
|
||||
}
|
||||
|
||||
params := client.buildResponseParams(msgs, opts)
|
||||
|
||||
assert.Nil(t, params.Tools, "Expected no tools when search is disabled")
|
||||
}
|
||||
|
||||
func TestCitationFormatting(t *testing.T) {
|
||||
// Test the citation formatting logic by simulating the citation extraction
|
||||
var textParts []string
|
||||
|
|
|
|||
|
|
@ -21,6 +21,14 @@ type ProviderConfig struct {
|
|||
BaseURL string
|
||||
ModelsURL string // Optional: Custom endpoint for listing models (if different from BaseURL/models)
|
||||
ImplementsResponses bool // Whether the provider supports OpenAI's new Responses API
|
||||
// WebSearchToolName overrides the default "web_search_preview" tool name
|
||||
// emitted on the Responses API when Search is enabled. Leave empty to keep
|
||||
// the OpenAI default. xAI, for example, requires "web_search".
|
||||
WebSearchToolName string
|
||||
// EnableXSearch, when true, also appends an xAI "x_search" tool entry
|
||||
// alongside the web search tool when Search is enabled. Non-xAI
|
||||
// providers should leave this false.
|
||||
EnableXSearch bool
|
||||
}
|
||||
|
||||
// Client is the common structure for all OpenAI-compatible providers
|
||||
|
|
@ -40,6 +48,10 @@ func NewClient(providerConfig ProviderConfig) *Client {
|
|||
providerConfig.ImplementsResponses,
|
||||
nil,
|
||||
)
|
||||
// Apply optional Responses API tool overrides. Zero values preserve
|
||||
// existing behavior for providers that do not set these fields.
|
||||
client.Client.SetWebSearchToolName(providerConfig.WebSearchToolName)
|
||||
client.Client.SetEnableXSearch(providerConfig.EnableXSearch)
|
||||
return client
|
||||
}
|
||||
|
||||
|
|
@ -240,6 +252,11 @@ var ProviderMap = map[string]ProviderConfig{
|
|||
Name: "GrokAI",
|
||||
BaseURL: "https://api.x.ai/v1",
|
||||
ImplementsResponses: true,
|
||||
// xAI's Responses API expects the "web_search" tool type, not
|
||||
// OpenAI's "web_search_preview", and additionally accepts an
|
||||
// "x_search" tool entry for live search grounding.
|
||||
WebSearchToolName: "web_search",
|
||||
EnableXSearch: true,
|
||||
},
|
||||
"Groq": {
|
||||
Name: "Groq",
|
||||
|
|
|
|||
Loading…
Reference in a new issue