fix: 适配claude code调度openai账号的websearch功能

This commit is contained in:
shaw
2026-03-07 11:27:00 +08:00
parent c0c322ba16
commit 6411645ffc
3 changed files with 102 additions and 6 deletions

View File

@@ -325,16 +325,22 @@ func extractAnthropicTextFromBlocks(blocks []AnthropicContentBlock) string {
} }
// convertAnthropicToolsToResponses maps Anthropic tool definitions to // convertAnthropicToolsToResponses maps Anthropic tool definitions to
// Responses API function tools (input_schema → parameters). // Responses API tools. Server-side tools like web_search are mapped to their
// OpenAI equivalents; regular tools become function tools.
func convertAnthropicToolsToResponses(tools []AnthropicTool) []ResponsesTool { func convertAnthropicToolsToResponses(tools []AnthropicTool) []ResponsesTool {
out := make([]ResponsesTool, len(tools)) var out []ResponsesTool
for i, t := range tools { for _, t := range tools {
out[i] = ResponsesTool{ // Anthropic server tools like "web_search_20250305" → OpenAI {"type":"web_search"}
if strings.HasPrefix(t.Type, "web_search") {
out = append(out, ResponsesTool{Type: "web_search"})
continue
}
out = append(out, ResponsesTool{
Type: "function", Type: "function",
Name: t.Name, Name: t.Name,
Description: t.Description, Description: t.Description,
Parameters: t.InputSchema, Parameters: t.InputSchema,
} })
} }
return out return out
} }

View File

@@ -54,6 +54,25 @@ func ResponsesToAnthropic(resp *ResponsesResponse, model string) *AnthropicRespo
Name: item.Name, Name: item.Name,
Input: json.RawMessage(item.Arguments), Input: json.RawMessage(item.Arguments),
}) })
case "web_search_call":
toolUseID := "srvtoolu_" + item.ID
query := ""
if item.Action != nil {
query = item.Action.Query
}
inputJSON, _ := json.Marshal(map[string]string{"query": query})
blocks = append(blocks, AnthropicContentBlock{
Type: "server_tool_use",
ID: toolUseID,
Name: "web_search",
Input: inputJSON,
})
emptyResults, _ := json.Marshal([]struct{}{})
blocks = append(blocks, AnthropicContentBlock{
Type: "web_search_tool_result",
ToolUseID: toolUseID,
Content: emptyResults,
})
} }
} }
@@ -369,12 +388,73 @@ func resToAnthHandleOutputItemDone(evt *ResponsesStreamEvent, state *ResponsesEv
if evt.Item == nil { if evt.Item == nil {
return nil return nil
} }
// Handle web_search_call → synthesize server_tool_use + web_search_tool_result blocks.
if evt.Item.Type == "web_search_call" && evt.Item.Status == "completed" {
return resToAnthHandleWebSearchDone(evt, state)
}
if state.ContentBlockOpen { if state.ContentBlockOpen {
return closeCurrentBlock(state) return closeCurrentBlock(state)
} }
return nil return nil
} }
// resToAnthHandleWebSearchDone converts an OpenAI web_search_call output item
// into Anthropic server_tool_use + web_search_tool_result content block pairs.
// This allows Claude Code to count the searches performed.
func resToAnthHandleWebSearchDone(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
var events []AnthropicStreamEvent
events = append(events, closeCurrentBlock(state)...)
toolUseID := "srvtoolu_" + evt.Item.ID
query := ""
if evt.Item.Action != nil {
query = evt.Item.Action.Query
}
inputJSON, _ := json.Marshal(map[string]string{"query": query})
// Emit server_tool_use block (start + stop).
idx1 := state.ContentBlockIndex
events = append(events, AnthropicStreamEvent{
Type: "content_block_start",
Index: &idx1,
ContentBlock: &AnthropicContentBlock{
Type: "server_tool_use",
ID: toolUseID,
Name: "web_search",
Input: inputJSON,
},
})
events = append(events, AnthropicStreamEvent{
Type: "content_block_stop",
Index: &idx1,
})
state.ContentBlockIndex++
// Emit web_search_tool_result block (start + stop).
// Content is empty because OpenAI does not expose individual search results;
// the model consumes them internally and produces text output.
emptyResults, _ := json.Marshal([]struct{}{})
idx2 := state.ContentBlockIndex
events = append(events, AnthropicStreamEvent{
Type: "content_block_start",
Index: &idx2,
ContentBlock: &AnthropicContentBlock{
Type: "web_search_tool_result",
ToolUseID: toolUseID,
Content: emptyResults,
},
})
events = append(events, AnthropicStreamEvent{
Type: "content_block_stop",
Index: &idx2,
})
state.ContentBlockIndex++
return events
}
func resToAnthHandleCompleted(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent { func resToAnthHandleCompleted(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
if state.MessageStopSent { if state.MessageStopSent {
return nil return nil

View File

@@ -60,6 +60,7 @@ type AnthropicContentBlock struct {
// AnthropicTool describes a tool available to the model. // AnthropicTool describes a tool available to the model.
type AnthropicTool struct { type AnthropicTool struct {
Type string `json:"type,omitempty"` // e.g. "web_search_20250305" for server tools
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
InputSchema json.RawMessage `json:"input_schema"` // JSON Schema object InputSchema json.RawMessage `json:"input_schema"` // JSON Schema object
@@ -217,7 +218,7 @@ type ResponsesIncompleteDetails struct {
// ResponsesOutput is one output item in a Responses API response. // ResponsesOutput is one output item in a Responses API response.
type ResponsesOutput struct { type ResponsesOutput struct {
Type string `json:"type"` // "message" | "reasoning" | "function_call" Type string `json:"type"` // "message" | "reasoning" | "function_call" | "web_search_call"
// type=message // type=message
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
@@ -233,6 +234,15 @@ type ResponsesOutput struct {
CallID string `json:"call_id,omitempty"` CallID string `json:"call_id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"` Arguments string `json:"arguments,omitempty"`
// type=web_search_call
Action *WebSearchAction `json:"action,omitempty"`
}
// WebSearchAction describes the search action in a web_search_call output item.
type WebSearchAction struct {
Type string `json:"type,omitempty"` // "search"
Query string `json:"query,omitempty"` // primary search query
} }
// ResponsesSummary is a summary text block inside a reasoning output. // ResponsesSummary is a summary text block inside a reasoning output.