fix: 适配claude code调度openai账号的websearch功能
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user