@@ -58,7 +58,16 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
5858 }
5959 translated = applyPayloadConfigWithRoot (e .cfg , req .Model , to .String (), "" , translated )
6060
61- url := strings .TrimSuffix (baseURL , "/" ) + "/chat/completions"
61+ // Check if this is a web search request (has special marker we added in translator)
62+ isWebSearch := isWebSearchRequest (translated )
63+
64+ var url string
65+ if isWebSearch {
66+ url = strings .TrimSuffix (baseURL , "/" ) + "/chat/retrieve"
67+ } else {
68+ url = strings .TrimSuffix (baseURL , "/" ) + "/chat/completions"
69+ }
70+
6271 httpReq , err := http .NewRequestWithContext (ctx , http .MethodPost , url , bytes .NewReader (translated ))
6372 if err != nil {
6473 return resp , err
@@ -116,12 +125,23 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
116125 return resp , err
117126 }
118127 appendAPIResponseChunk (ctx , e .cfg , body )
119- reporter .publish (ctx , parseOpenAIUsage (body ))
120- // Ensure we at least record the request even if upstream doesn't return usage
121- reporter .ensurePublished (ctx )
122- // Translate response back to source format when needed
128+
129+ // Handle web search responses differently from standard OpenAI responses
130+ var out string
123131 var param any
124- out := sdktranslator .TranslateNonStream (ctx , to , from , req .Model , bytes .Clone (opts .OriginalRequest ), translated , body , & param )
132+ if isWebSearch {
133+ // For web search responses, we need to format them properly for Claude
134+ // The /chat/retrieve endpoint returns a different format than OpenAI
135+ out = sdktranslator .TranslateNonStream (ctx , to , from , req .Model , bytes .Clone (opts .OriginalRequest ), translated , body , & param )
136+ } else {
137+ // Standard OpenAI response handling
138+ reporter .publish (ctx , parseOpenAIUsage (body ))
139+ // Ensure we at least record the request even if upstream doesn't return usage
140+ reporter .ensurePublished (ctx )
141+ // Translate response back to source format when needed
142+ out = sdktranslator .TranslateNonStream (ctx , to , from , req .Model , bytes .Clone (opts .OriginalRequest ), translated , body , & param )
143+ }
144+
125145 resp = cliproxyexecutor.Response {Payload : []byte (out )}
126146 return resp , nil
127147}
@@ -143,7 +163,16 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
143163 }
144164 translated = applyPayloadConfigWithRoot (e .cfg , req .Model , to .String (), "" , translated )
145165
146- url := strings .TrimSuffix (baseURL , "/" ) + "/chat/completions"
166+ // Check if this is a web search request (has special marker we added in translator)
167+ isWebSearch := isWebSearchRequest (translated )
168+
169+ var url string
170+ if isWebSearch {
171+ url = strings .TrimSuffix (baseURL , "/" ) + "/chat/retrieve"
172+ } else {
173+ url = strings .TrimSuffix (baseURL , "/" ) + "/chat/completions"
174+ }
175+
147176 httpReq , err := http .NewRequestWithContext (ctx , http .MethodPost , url , bytes .NewReader (translated ))
148177 if err != nil {
149178 return nil , err
@@ -158,8 +187,12 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
158187 attrs = auth .Attributes
159188 }
160189 util .ApplyCustomHeadersFromAttrs (httpReq , attrs )
161- httpReq .Header .Set ("Accept" , "text/event-stream" )
162- httpReq .Header .Set ("Cache-Control" , "no-cache" )
190+
191+ // For web search, we don't want stream headers as it returns a complete response
192+ if ! isWebSearch {
193+ httpReq .Header .Set ("Accept" , "text/event-stream" )
194+ httpReq .Header .Set ("Cache-Control" , "no-cache" )
195+ }
163196 var authID , authLabel , authType , authValue string
164197 if auth != nil {
165198 authID = auth .ID
@@ -195,6 +228,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
195228 err = statusErr {code : httpResp .StatusCode , msg : string (b )}
196229 return nil , err
197230 }
231+
198232 out := make (chan cliproxyexecutor.StreamChunk )
199233 stream = out
200234 go func () {
@@ -204,33 +238,57 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
204238 log .Errorf ("openai compat executor: close response body error: %v" , errClose )
205239 }
206240 }()
207- scanner := bufio .NewScanner (httpResp .Body )
208- buf := make ([]byte , 20_971_520 )
209- scanner .Buffer (buf , 20_971_520 )
210- var param any
211- for scanner .Scan () {
212- line := scanner .Bytes ()
213- appendAPIResponseChunk (ctx , e .cfg , line )
214- if detail , ok := parseOpenAIStreamUsage (line ); ok {
215- reporter .publish (ctx , detail )
216- }
217- if len (line ) == 0 {
218- continue
241+
242+ // For web search requests, the response is a single JSON rather than an SSE stream
243+ if isWebSearch {
244+ // Read the complete response body at once, since /chat/retrieve returns complete JSON
245+ body , err := io .ReadAll (httpResp .Body )
246+ if err != nil {
247+ recordAPIResponseError (ctx , e .cfg , err )
248+ reporter .publishFailure (ctx )
249+ out <- cliproxyexecutor.StreamChunk {Err : err }
250+ return
219251 }
220- // OpenAI-compatible streams are SSE: lines typically prefixed with "data: ".
221- // Pass through translator; it yields one or more chunks for the target schema.
222- chunks := sdktranslator .TranslateStream (ctx , to , from , req .Model , bytes .Clone (opts .OriginalRequest ), translated , bytes .Clone (line ), & param )
252+
253+ appendAPIResponseChunk (ctx , e .cfg , body )
254+
255+ // Translate the single web search response
256+ // The response translator should handle web search response format
257+ var param any
258+ chunks := sdktranslator .TranslateStream (ctx , to , from , req .Model , bytes .Clone (opts .OriginalRequest ), translated , body , & param )
223259 for i := range chunks {
224260 out <- cliproxyexecutor.StreamChunk {Payload : []byte (chunks [i ])}
225261 }
262+ } else {
263+ // For regular OpenAI-compatible streaming responses
264+ scanner := bufio .NewScanner (httpResp .Body )
265+ buf := make ([]byte , 20_971_520 )
266+ scanner .Buffer (buf , 20_971_520 )
267+ var param any
268+ for scanner .Scan () {
269+ line := scanner .Bytes ()
270+ appendAPIResponseChunk (ctx , e .cfg , line )
271+ if detail , ok := parseOpenAIStreamUsage (line ); ok {
272+ reporter .publish (ctx , detail )
273+ }
274+ if len (line ) == 0 {
275+ continue
276+ }
277+ // OpenAI-compatible streams are SSE: lines typically prefixed with "data: ".
278+ // Pass through translator; it yields one or more chunks for the target schema.
279+ chunks := sdktranslator .TranslateStream (ctx , to , from , req .Model , bytes .Clone (opts .OriginalRequest ), translated , bytes .Clone (line ), & param )
280+ for i := range chunks {
281+ out <- cliproxyexecutor.StreamChunk {Payload : []byte (chunks [i ])}
282+ }
283+ }
284+ if errScan := scanner .Err (); errScan != nil {
285+ recordAPIResponseError (ctx , e .cfg , errScan )
286+ reporter .publishFailure (ctx )
287+ out <- cliproxyexecutor.StreamChunk {Err : errScan }
288+ }
289+ // Ensure we record the request if no usage chunk was ever seen
290+ reporter .ensurePublished (ctx )
226291 }
227- if errScan := scanner .Err (); errScan != nil {
228- recordAPIResponseError (ctx , e .cfg , errScan )
229- reporter .publishFailure (ctx )
230- out <- cliproxyexecutor.StreamChunk {Err : errScan }
231- }
232- // Ensure we record the request if no usage chunk was ever seen
233- reporter .ensurePublished (ctx )
234292 }()
235293 return stream , nil
236294}
@@ -352,3 +410,11 @@ func (e statusErr) Error() string {
352410 return fmt .Sprintf ("status %d" , e .code )
353411}
354412func (e statusErr ) StatusCode () int { return e .code }
413+
414+ // isWebSearchRequest checks if the translated request is a web search request
415+ // by looking for the special marker we add in ConvertClaudeRequestToOpenAI
416+ func isWebSearchRequest (translated []byte ) bool {
417+ // Check if the translated request has the web search marker
418+ // This looks for the "_web_search_request":true field we add
419+ return bytes .Contains (translated , []byte ("\" _web_search_request\" :true" ))
420+ }
0 commit comments