mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
Add model-native search (prefer_native) for OpenAI/Codex (#1618)
* config: add prefer_native and NativeSearchCapable for model-native search * providers: implement native web search for OpenAI and Codex * agent: use provider-native search when prefer_native and supported * tests: add coverage for model-native search * fix: Golang lint errors * fix: update the code based on the review * fix: update codex_provider_test
This commit is contained in:
@@ -1037,6 +1037,19 @@ func (al *AgentLoop) runLLMIteration(
|
||||
// Build tool definitions
|
||||
providerToolDefs := agent.Tools.ToProviderDefs()
|
||||
|
||||
// Determine whether the provider's native web search should replace
|
||||
// the client-side web_search tool for this request. Only enable when web
|
||||
// search is actually enabled and registered (so users who disabled web
|
||||
// access do not get provider-side search or billing).
|
||||
_, hasWebSearch := agent.Tools.Get("web_search")
|
||||
useNativeSearch := al.cfg.Tools.Web.PreferNative &&
|
||||
isNativeSearchProvider(agent.Provider) &&
|
||||
hasWebSearch
|
||||
|
||||
if useNativeSearch {
|
||||
providerToolDefs = filterClientWebSearch(providerToolDefs)
|
||||
}
|
||||
|
||||
// Log LLM request details
|
||||
logger.DebugCF("agent", "LLM request",
|
||||
map[string]any{
|
||||
@@ -1045,6 +1058,7 @@ func (al *AgentLoop) runLLMIteration(
|
||||
"model": activeModel,
|
||||
"messages_count": len(messages),
|
||||
"tools_count": len(providerToolDefs),
|
||||
"native_search": useNativeSearch,
|
||||
"max_tokens": agent.MaxTokens,
|
||||
"temperature": agent.Temperature,
|
||||
"system_prompt_len": len(messages[0].Content),
|
||||
@@ -1067,6 +1081,9 @@ func (al *AgentLoop) runLLMIteration(
|
||||
"temperature": agent.Temperature,
|
||||
"prompt_cache_key": agent.ID,
|
||||
}
|
||||
if useNativeSearch {
|
||||
llmOpts["native_search"] = true
|
||||
}
|
||||
// parseThinkingLevel guarantees ThinkingOff for empty/unknown values,
|
||||
// so checking != ThinkingOff is sufficient.
|
||||
if agent.ThinkingLevel != ThinkingOff {
|
||||
@@ -1976,6 +1993,28 @@ func extractParentPeer(msg bus.InboundMessage) *routing.RoutePeer {
|
||||
return &routing.RoutePeer{Kind: parentKind, ID: parentID}
|
||||
}
|
||||
|
||||
// isNativeSearchProvider reports whether the given LLM provider implements
|
||||
// NativeSearchCapable and returns true for SupportsNativeSearch.
|
||||
func isNativeSearchProvider(p providers.LLMProvider) bool {
|
||||
if ns, ok := p.(providers.NativeSearchCapable); ok {
|
||||
return ns.SupportsNativeSearch()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// filterClientWebSearch returns a copy of tools with the client-side
|
||||
// web_search tool removed. Used when native provider search is preferred.
|
||||
func filterClientWebSearch(tools []providers.ToolDefinition) []providers.ToolDefinition {
|
||||
result := make([]providers.ToolDefinition, 0, len(tools))
|
||||
for _, t := range tools {
|
||||
if strings.EqualFold(t.Function.Name, "web_search") {
|
||||
continue
|
||||
}
|
||||
result = append(result, t)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Helper to extract provider from registry for cleanup
|
||||
func extractProvider(registry *AgentRegistry) (providers.LLMProvider, bool) {
|
||||
if registry == nil {
|
||||
|
||||
@@ -1426,3 +1426,84 @@ func TestResolveMediaRefs_MixedImageAndFile(t *testing.T) {
|
||||
t.Fatalf("expected content %q, got %q", expectedContent, result[0].Content)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Native search helper tests ---
|
||||
|
||||
type nativeSearchProvider struct {
|
||||
supported bool
|
||||
}
|
||||
|
||||
func (p *nativeSearchProvider) Chat(
|
||||
ctx context.Context, msgs []providers.Message, tools []providers.ToolDefinition,
|
||||
model string, opts map[string]any,
|
||||
) (*providers.LLMResponse, error) {
|
||||
return &providers.LLMResponse{Content: "ok"}, nil
|
||||
}
|
||||
|
||||
func (p *nativeSearchProvider) GetDefaultModel() string { return "test-model" }
|
||||
|
||||
func (p *nativeSearchProvider) SupportsNativeSearch() bool { return p.supported }
|
||||
|
||||
type plainProvider struct{}
|
||||
|
||||
func (p *plainProvider) Chat(
|
||||
ctx context.Context, msgs []providers.Message, tools []providers.ToolDefinition,
|
||||
model string, opts map[string]any,
|
||||
) (*providers.LLMResponse, error) {
|
||||
return &providers.LLMResponse{Content: "ok"}, nil
|
||||
}
|
||||
|
||||
func (p *plainProvider) GetDefaultModel() string { return "test-model" }
|
||||
|
||||
func TestIsNativeSearchProvider_Supported(t *testing.T) {
|
||||
if !isNativeSearchProvider(&nativeSearchProvider{supported: true}) {
|
||||
t.Fatal("expected true for provider that supports native search")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNativeSearchProvider_NotSupported(t *testing.T) {
|
||||
if isNativeSearchProvider(&nativeSearchProvider{supported: false}) {
|
||||
t.Fatal("expected false for provider that does not support native search")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNativeSearchProvider_NoInterface(t *testing.T) {
|
||||
if isNativeSearchProvider(&plainProvider{}) {
|
||||
t.Fatal("expected false for provider that does not implement NativeSearchCapable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterClientWebSearch_RemovesWebSearch(t *testing.T) {
|
||||
defs := []providers.ToolDefinition{
|
||||
{Type: "function", Function: providers.ToolFunctionDefinition{Name: "web_search"}},
|
||||
{Type: "function", Function: providers.ToolFunctionDefinition{Name: "read_file"}},
|
||||
{Type: "function", Function: providers.ToolFunctionDefinition{Name: "exec"}},
|
||||
}
|
||||
result := filterClientWebSearch(defs)
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("len(result) = %d, want 2", len(result))
|
||||
}
|
||||
for _, td := range result {
|
||||
if td.Function.Name == "web_search" {
|
||||
t.Fatal("web_search should be filtered out")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterClientWebSearch_NoWebSearch(t *testing.T) {
|
||||
defs := []providers.ToolDefinition{
|
||||
{Type: "function", Function: providers.ToolFunctionDefinition{Name: "read_file"}},
|
||||
{Type: "function", Function: providers.ToolFunctionDefinition{Name: "exec"}},
|
||||
}
|
||||
result := filterClientWebSearch(defs)
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("len(result) = %d, want 2", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterClientWebSearch_EmptyInput(t *testing.T) {
|
||||
result := filterClientWebSearch(nil)
|
||||
if len(result) != 0 {
|
||||
t.Fatalf("len(result) = %d, want 0", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user