返回 DeepSeek-Reasonix
web_search_stream.go
根目录 / internal / provider / anthropic / web_search_stream.go
1 package anthropic
2
3 import (
4 "encoding/json"
5
6 "reasonix/internal/provider"
7 )
8
9 type searchBlock struct {
10 call provider.ServerSearchCall
11 args string
12 }
13
14 type searchStream struct {
15 byIndex map[int]*searchBlock
16 byID map[string]*searchBlock
17 }
18
19 func newSearchStream() *searchStream {
20 return &searchStream{byIndex: map[int]*searchBlock{}, byID: map[string]*searchBlock{}}
21 }
22
23 func appendServerSearchBlocks(blocks []contentBlock, searches []provider.ServerSearchCall) []contentBlock {
24 for _, search := range searches {
25 if search.ID == "" {
26 continue
27 }
28 input := json.RawMessage(`{}`)
29 if search.Query != "" {
30 if raw, err := json.Marshal(map[string]string{"query": search.Query}); err == nil {
31 input = raw
32 }
33 }
34 blocks = append(blocks, contentBlock{Type: "server_tool_use", ID: search.ID, Name: "web_search", Input: input})
35 raw := search.Raw
36 if len(raw) == 0 {
37 raw = json.RawMessage("[]")
38 }
39 blocks = append(blocks, contentBlock{Type: "web_search_tool_result", ToolUseID: search.ID, Content: json.RawMessage(append(json.RawMessage(nil), raw...))})
40 }
41 return blocks
42 }
43
44 type streamContentBlock struct {
45 Thinking string `json:"thinking"`
46 Signature string `json:"signature"`
47 Data string `json:"data"`
48 Type string `json:"type"`
49 ID string `json:"id"`
50 Name string `json:"name"`
51 ToolUseID string `json:"tool_use_id"`
52 Content json.RawMessage `json:"content"`
53 }
54
55 func beginContentBlock(index int, block *streamContentBlock, tools map[int]*provider.ToolCall, searches *searchStream) *provider.Chunk {
56 if block == nil {
57 return nil
58 }
59 switch block.Type {
60 case "tool_use":
61 tc := &provider.ToolCall{ID: block.ID, Name: block.Name}
62 tools[index] = tc
63 return &provider.Chunk{Type: provider.ChunkToolCallStart, ToolCall: &provider.ToolCall{ID: tc.ID, Name: tc.Name}}
64 case "server_tool_use":
65 if started := searches.start(index, block.ID, block.Name); started != nil {
66 return &provider.Chunk{Type: provider.ChunkServerSearch, ServerSearch: started}
67 }
68 case "web_search_tool_result":
69 if result := searches.result(block.ToolUseID, block.Content); result != nil {
70 // Register the result block's stream index too: streams that do not
71 // inline the content deliver it via web_search_tool_result_delta.
72 searches.byIndex[index] = searches.byID[block.ToolUseID]
73 return &provider.Chunk{Type: provider.ChunkServerSearch, ServerSearch: result}
74 }
75 }
76 return nil
77 }
78
79 func (s *searchStream) start(index int, id, name string) *provider.ServerSearchCall {
80 if name != "web_search" || id == "" {
81 return nil
82 }
83 block := &searchBlock{call: provider.ServerSearchCall{ID: id}}
84 s.byIndex[index] = block
85 s.byID[id] = block
86 return &provider.ServerSearchCall{ID: id}
87 }
88
89 func (s *searchStream) result(id string, raw json.RawMessage) *provider.ServerSearchCall {
90 block := s.byID[id]
91 if block == nil {
92 block = &searchBlock{call: provider.ServerSearchCall{ID: id}}
93 if id != "" {
94 s.byID[id] = block
95 }
96 }
97 if len(raw) > 0 {
98 block.call.Raw = append(json.RawMessage(nil), raw...)
99 block.call.Results = provider.ParseServerSearchHits(raw)
100 }
101 return cloneServerSearch(block.call)
102 }
103
104 // resultsDelta ingests a web_search_tool_result_delta payload: the result
105 // array arrives after block start on streams that do not inline it in the
106 // web_search_tool_result content_block_start content.
107 func (s *searchStream) resultsDelta(index int, raw json.RawMessage) *provider.ServerSearchCall {
108 block := s.byIndex[index]
109 if block == nil || len(raw) == 0 {
110 return nil
111 }
112 block.call.Raw = append(json.RawMessage(nil), raw...)
113 block.call.Results = provider.ParseServerSearchHits(raw)
114 return cloneServerSearch(block.call)
115 }
116
117 func (s *searchStream) argsDelta(index int, partial string) *provider.ServerSearchCall {
118 block := s.byIndex[index]
119 if block == nil || partial == "" {
120 return nil
121 }
122 block.args += partial
123 query := provider.ParseServerSearchQuery(block.args)
124 if query == "" {
125 return nil
126 }
127 block.call.Query = query
128 return &provider.ServerSearchCall{ID: block.call.ID, Query: query}
129 }
130
131 func cloneServerSearch(call provider.ServerSearchCall) *provider.ServerSearchCall {
132 out := call
133 if len(call.Results) > 0 {
134 out.Results = append([]provider.ServerSearchHit(nil), call.Results...)
135 }
136 if len(call.Raw) > 0 {
137 out.Raw = append(json.RawMessage(nil), call.Raw...)
138 }
139 return &out
140 }
141
142 func formatWebSearchResults(raw json.RawMessage) string {
143 return provider.FormatServerSearchFootnotes(provider.ParseServerSearchHits(raw))
144 }
145
145 lines GO