mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 06:35:00 +08:00
Add a native Antigravity WebSearch path for Claude typed WebSearch requests. Detect Claude Messages requests whose tools are only typed WebSearch tools (web_search_20250305 / web_search_20260209), and convert them into an Antigravity requestType=web_search payload instead of sending the request through the normal tool-calling path. Preserve the user's requested model. The native path is enabled only when that Antigravity model is known to support Google Search. Capability data fetched from Antigravity model info is used only as an enhancement to the local model registry, not as a replacement for the existing registry fallback behavior. Unsupported models keep the existing Antigravity request behavior and are not silently rerouted to another web-search-capable model. Translate Claude WebSearch request options to the verified Antigravity googleSearch shape: - max_uses -> googleSearch.enhancedContent.imageSearch.maxResultCount - allowed_domains -> googleSearch.includedDomains Leave blocked_domains and user_location unmapped because the Antigravity googleSearch request shape has no verified equivalent for them. This avoids sending speculative fields or pretending unsupported Claude WebSearch options are enforced upstream. Translate Antigravity web-search responses back into Claude-compatible output: server_tool_use blocks, web_search_tool_result blocks, cited text blocks, grounding URLs, and usage-compatible stream/non-stream responses. Cover the behavior with tests for request conversion, response conversion, grounding URL resolution, domain filter mapping, fetched capability hints, excluded-model handling, and unsupported-model behavior.
105 lines
3.0 KiB
Go
105 lines
3.0 KiB
Go
package helps
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
func isAntigravityVertexSearchRedirect(rawURL string) bool {
|
|
parsed, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return parsed.Scheme == "https" &&
|
|
parsed.Host == "vertexaisearch.cloud.google.com" &&
|
|
strings.HasPrefix(parsed.Path, "/grounding-api-redirect/")
|
|
}
|
|
|
|
func resolveAntigravityGroundingURL(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, rawURL string) string {
|
|
if !isAntigravityVertexSearchRedirect(rawURL) {
|
|
return rawURL
|
|
}
|
|
client := NewProxyAwareHTTPClient(ctx, cfg, auth, 0)
|
|
client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
}
|
|
req, errReq := http.NewRequestWithContext(ctx, http.MethodHead, rawURL, nil)
|
|
if errReq != nil {
|
|
log.WithError(errReq).Debug("antigravity grounding url: create redirect request failed")
|
|
return rawURL
|
|
}
|
|
resp, errDo := client.Do(req)
|
|
if errDo != nil {
|
|
log.WithError(errDo).Debug("antigravity grounding url: resolve redirect failed")
|
|
return rawURL
|
|
}
|
|
defer func() {
|
|
if errClose := resp.Body.Close(); errClose != nil {
|
|
log.WithError(errClose).Debug("antigravity grounding url: close redirect response failed")
|
|
}
|
|
}()
|
|
|
|
if resp.StatusCode < http.StatusMultipleChoices || resp.StatusCode >= http.StatusBadRequest {
|
|
return rawURL
|
|
}
|
|
location := strings.TrimSpace(resp.Header.Get("Location"))
|
|
if location == "" {
|
|
return rawURL
|
|
}
|
|
parsed, errParse := url.Parse(location)
|
|
if errParse != nil || parsed.Scheme != "https" || parsed.Host == "" {
|
|
return rawURL
|
|
}
|
|
return location
|
|
}
|
|
|
|
// ResolveAntigravityGroundingURLs replaces Vertex Search redirect URLs in grounding chunks with their target URLs.
|
|
func ResolveAntigravityGroundingURLs(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, payload []byte) []byte {
|
|
if len(payload) == 0 {
|
|
return payload
|
|
}
|
|
|
|
basePath := "response.candidates.0.groundingMetadata.groundingChunks"
|
|
chunks := gjson.GetBytes(payload, basePath)
|
|
if !chunks.IsArray() {
|
|
basePath = "candidates.0.groundingMetadata.groundingChunks"
|
|
chunks = gjson.GetBytes(payload, basePath)
|
|
}
|
|
if !chunks.IsArray() {
|
|
return payload
|
|
}
|
|
|
|
output := payload
|
|
resolved := map[string]string{}
|
|
for i, chunk := range chunks.Array() {
|
|
uri := strings.TrimSpace(chunk.Get("web.uri").String())
|
|
if uri == "" {
|
|
continue
|
|
}
|
|
resolvedURI, ok := resolved[uri]
|
|
if !ok {
|
|
resolvedURI = resolveAntigravityGroundingURL(ctx, cfg, auth, uri)
|
|
resolved[uri] = resolvedURI
|
|
}
|
|
if resolvedURI == uri {
|
|
continue
|
|
}
|
|
updated, errSet := sjson.SetBytes(output, fmt.Sprintf("%s.%d.web.uri", basePath, i), resolvedURI)
|
|
if errSet != nil {
|
|
log.WithError(errSet).Debug("antigravity grounding url: set resolved url failed")
|
|
continue
|
|
}
|
|
output = updated
|
|
}
|
|
return output
|
|
}
|