Skip to content

Commit 80d61a3

Browse files
committed
feat(antigravity): support native Claude web search
1 parent ac4017e commit 80d61a3

12 files changed

Lines changed: 1019 additions & 11 deletions

internal/registry/model_definitions.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package registry
44

55
import (
6+
"sort"
67
"strings"
78
)
89

@@ -85,6 +86,66 @@ func GetAntigravityModels() []*ModelInfo {
8586
return cloneModelInfos(getModels().Antigravity)
8687
}
8788

89+
// AntigravityWebSearchModels returns models listed by
90+
// fetchAvailableModels.webSearchModelIds.
91+
func AntigravityWebSearchModels() []string {
92+
out := make([]string, 0)
93+
for _, model := range GetGlobalRegistry().GetAvailableModelsByProvider("antigravity") {
94+
if model == nil || !model.SupportsWebSearch {
95+
continue
96+
}
97+
modelID := normalizeAntigravityCapabilityModelID(model.ID)
98+
if modelID != "" {
99+
out = append(out, modelID)
100+
}
101+
}
102+
sort.Strings(out)
103+
return out
104+
}
105+
106+
// AntigravityWebSearchModelFor returns the Antigravity model that should run a
107+
// native web search request for modelID.
108+
func AntigravityWebSearchModelFor(modelID string) string {
109+
modelID = normalizeAntigravityCapabilityModelID(modelID)
110+
if modelID == "" {
111+
return ""
112+
}
113+
for _, model := range GetGlobalRegistry().GetAvailableModelsByProvider("antigravity") {
114+
if model == nil {
115+
continue
116+
}
117+
currentModelID := normalizeAntigravityCapabilityModelID(model.ID)
118+
if currentModelID == "" {
119+
continue
120+
}
121+
if currentModelID == modelID {
122+
if model.SupportsWebSearch {
123+
return currentModelID
124+
}
125+
return ""
126+
}
127+
}
128+
return ""
129+
}
130+
131+
// IsAntigravityWebSearchModel reports whether an Antigravity model is listed by
132+
// fetchAvailableModels.webSearchModelIds and can run the native googleSearch tool.
133+
func IsAntigravityWebSearchModel(modelID string) bool {
134+
modelID = normalizeAntigravityCapabilityModelID(modelID)
135+
if modelID == "" {
136+
return false
137+
}
138+
for _, model := range GetGlobalRegistry().GetAvailableModelsByProvider("antigravity") {
139+
if model == nil {
140+
continue
141+
}
142+
if normalizeAntigravityCapabilityModelID(model.ID) == modelID {
143+
return model.SupportsWebSearch
144+
}
145+
}
146+
return false
147+
}
148+
88149
// GetXAIModels returns the standard xAI Grok model definitions.
89150
func GetXAIModels() []*ModelInfo {
90151
return WithXAIBuiltins(cloneModelInfos(getModels().XAI))
@@ -103,6 +164,14 @@ func WithXAIBuiltins(models []*ModelInfo) []*ModelInfo {
103164
return upsertModelInfos(models, xaiBuiltinImageModelInfo(), xaiBuiltinImageQualityModelInfo(), xaiBuiltinVideoModelInfo(), xaiBuiltinVideo15PreviewModelInfo())
104165
}
105166

167+
func normalizeAntigravityCapabilityModelID(modelID string) string {
168+
modelID = strings.ToLower(strings.TrimSpace(modelID))
169+
if open := strings.LastIndex(modelID, "("); open >= 0 && strings.HasSuffix(modelID, ")") {
170+
modelID = strings.TrimSpace(modelID[:open])
171+
}
172+
return modelID
173+
}
174+
106175
func codexBuiltinImageModelInfo() *ModelInfo {
107176
return &ModelInfo{
108177
ID: codexBuiltinImageModelID,

internal/registry/model_definitions_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,85 @@ func TestWithXAIBuiltinsIncludesVideoPreviewModel(t *testing.T) {
1616

1717
t.Fatalf("expected xAI builtin model %s", xaiBuiltinVideo15PreviewModelID)
1818
}
19+
20+
func TestIsAntigravityWebSearchModelUsesRuntimeCapability(t *testing.T) {
21+
registryRef := GetGlobalRegistry()
22+
registryRef.RegisterClient("test-antigravity-websearch", "antigravity", []*ModelInfo{
23+
{ID: "gemini-web-search-test", SupportsWebSearch: true},
24+
{ID: "gemini-web-search-disabled", SupportsWebSearch: false},
25+
})
26+
registryRef.RegisterClient("test-gemini-websearch", "gemini", []*ModelInfo{
27+
{ID: "gemini-web-search-cross-provider", SupportsWebSearch: true},
28+
})
29+
t.Cleanup(func() {
30+
registryRef.UnregisterClient("test-antigravity-websearch")
31+
registryRef.UnregisterClient("test-gemini-websearch")
32+
})
33+
34+
if !IsAntigravityWebSearchModel("gemini-web-search-test") {
35+
t.Fatal("runtime Antigravity web search model should be marked capable")
36+
}
37+
if !IsAntigravityWebSearchModel("gemini-web-search-test(high)") {
38+
t.Fatal("thinking suffix should not hide Antigravity web search capability")
39+
}
40+
if IsAntigravityWebSearchModel("gemini-web-search-disabled") {
41+
t.Fatal("Antigravity model without web search support should not be marked capable")
42+
}
43+
if IsAntigravityWebSearchModel("gemini-web-search-cross-provider") {
44+
t.Fatal("same capability on another provider should not mark Antigravity capable")
45+
}
46+
}
47+
48+
func TestAntigravityWebSearchModelsUsesRuntimeCapability(t *testing.T) {
49+
registryRef := GetGlobalRegistry()
50+
registryRef.RegisterClient("test-antigravity-websearch-list", "antigravity", []*ModelInfo{
51+
{ID: "gemini-web-search-test", SupportsWebSearch: true},
52+
{ID: "gemini-web-search-disabled", SupportsWebSearch: false},
53+
})
54+
t.Cleanup(func() {
55+
registryRef.UnregisterClient("test-antigravity-websearch-list")
56+
})
57+
58+
models := AntigravityWebSearchModels()
59+
if len(models) == 0 {
60+
t.Fatal("expected at least one Antigravity web search model")
61+
}
62+
for _, model := range models {
63+
if model == "gemini-web-search-test" {
64+
return
65+
}
66+
}
67+
t.Fatalf("AntigravityWebSearchModels() = %#v, want gemini-web-search-test", models)
68+
}
69+
70+
func TestAntigravityWebSearchModelForRequiresRequestedModelCapability(t *testing.T) {
71+
registryRef := GetGlobalRegistry()
72+
registryRef.RegisterClient("test-antigravity-websearch-route", "antigravity", []*ModelInfo{
73+
{ID: "gemini-route-test"},
74+
{ID: "gemini-web-search-test", SupportsWebSearch: true},
75+
})
76+
registryRef.RegisterClient("test-gemini-websearch-route", "gemini", []*ModelInfo{
77+
{ID: "gemini-cross-provider-route"},
78+
{ID: "gemini-cross-provider-search", SupportsWebSearch: true},
79+
})
80+
t.Cleanup(func() {
81+
registryRef.UnregisterClient("test-antigravity-websearch-route")
82+
registryRef.UnregisterClient("test-gemini-websearch-route")
83+
})
84+
85+
if got := AntigravityWebSearchModelFor("gemini-route-test"); got != "" {
86+
t.Fatalf("route model without web search support should not get fallback model, got %q", got)
87+
}
88+
if got := AntigravityWebSearchModelFor("gemini-route-test(high)"); got != "" {
89+
t.Fatalf("suffix route model without web search support should not get fallback model, got %q", got)
90+
}
91+
if got := AntigravityWebSearchModelFor("gemini-web-search-test"); got != "gemini-web-search-test" {
92+
t.Fatalf("AntigravityWebSearchModelFor capable model = %q, want itself", got)
93+
}
94+
if got := AntigravityWebSearchModelFor("gemini-cross-provider-route"); got != "" {
95+
t.Fatalf("cross-provider model should not get Antigravity web search model, got %q", got)
96+
}
97+
if got := AntigravityWebSearchModelFor("unknown-model"); got != "" {
98+
t.Fatalf("unknown model should not get Antigravity web search model, got %q", got)
99+
}
100+
}

internal/registry/model_registry.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ type ModelInfo struct {
5454
SupportedInputModalities []string `json:"supportedInputModalities,omitempty"`
5555
// SupportedOutputModalities lists supported output modalities (e.g., TEXT, IMAGE)
5656
SupportedOutputModalities []string `json:"supportedOutputModalities,omitempty"`
57+
// SupportsWebSearch indicates the provider can execute a native web search
58+
// tool for this model.
59+
SupportsWebSearch bool `json:"supports_web_search,omitempty"`
5760

5861
// Thinking holds provider-specific reasoning/thinking budget capabilities.
5962
// This is optional and currently used for Gemini thinking budget normalization.

internal/runtime/executor/antigravity_executor.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2050,11 +2050,15 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
20502050
return nil, errProject
20512051
}
20522052
payload = geminiToAntigravity(modelName, payload, projectID)
2053-
payload, _ = sjson.SetBytes(payload, "model", modelName)
2053+
effectiveModelName := strings.TrimSpace(gjson.GetBytes(payload, "model").String())
2054+
if effectiveModelName == "" {
2055+
effectiveModelName = modelName
2056+
payload, _ = sjson.SetBytes(payload, "model", effectiveModelName)
2057+
}
20542058

20552059
// Cap maxOutputTokens to model's max_completion_tokens from registry
20562060
if maxOut := gjson.GetBytes(payload, "request.generationConfig.maxOutputTokens"); maxOut.Exists() && maxOut.Type == gjson.Number {
2057-
if modelInfo := registry.LookupModelInfo(modelName, "antigravity"); modelInfo != nil && modelInfo.MaxCompletionTokens > 0 {
2061+
if modelInfo := registry.LookupModelInfo(effectiveModelName, "antigravity"); modelInfo != nil && modelInfo.MaxCompletionTokens > 0 {
20582062
if int(maxOut.Int()) > modelInfo.MaxCompletionTokens {
20592063
payload, _ = sjson.SetBytes(payload, "request.generationConfig.maxOutputTokens", modelInfo.MaxCompletionTokens)
20602064
}
@@ -2464,11 +2468,15 @@ func resolveCustomAntigravityBaseURL(auth *cliproxyauth.Auth) string {
24642468
}
24652469

24662470
func geminiToAntigravity(modelName string, payload []byte, projectID string) []byte {
2471+
effectiveModelName := strings.TrimSpace(gjson.GetBytes(payload, "model").String())
2472+
if effectiveModelName == "" {
2473+
effectiveModelName = modelName
2474+
}
24672475
template := payload
2468-
template, _ = sjson.SetBytes(template, "model", modelName)
2476+
template, _ = sjson.SetBytes(template, "model", effectiveModelName)
24692477
template, _ = sjson.SetBytes(template, "userAgent", "antigravity")
24702478

2471-
isImageModel := strings.Contains(modelName, "image")
2479+
isImageModel := strings.Contains(effectiveModelName, "image")
24722480

24732481
var reqType string
24742482
if isImageModel {

internal/runtime/executor/antigravity_executor_buildrequest_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,25 @@ func TestAntigravityBuildRequest_UsesAuthProjectID(t *testing.T) {
110110
}
111111
}
112112

113+
func TestAntigravityBuildRequest_PreservesTranslatedModel(t *testing.T) {
114+
body := buildRequestBodyFromRawPayload(t, "gemini-3-flash-agent", []byte(`{
115+
"model": "gemini-3.1-flash-lite",
116+
"request": {
117+
"contents": [
118+
{
119+
"role": "user",
120+
"parts": [{"text": "Perform a web search"}]
121+
}
122+
],
123+
"tools": [{"googleSearch": {}}]
124+
}
125+
}`))
126+
127+
if got, ok := body["model"].(string); !ok || got != "gemini-3.1-flash-lite" {
128+
t.Fatalf("translated model should be preserved, got=%v", body["model"])
129+
}
130+
}
131+
113132
func TestAntigravityPrepareRequestAuth_FetchesMissingProjectID(t *testing.T) {
114133
executor := &AntigravityExecutor{}
115134
auth := &cliproxyauth.Auth{Metadata: map[string]any{

internal/translator/antigravity/claude/antigravity_claude_request.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -592,13 +592,21 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
592592
// tools
593593
var toolsJSON []byte
594594
toolDeclCount := 0
595+
googleSearchToolCount := 0
596+
googleSearchModelName := antigravityNativeGoogleSearchModel(modelName)
595597
allowedToolKeys := []string{"name", "description", "behavior", "parameters", "parametersJsonSchema", "response", "responseJsonSchema"}
596598
toolsResult := gjson.GetBytes(rawJSON, "tools")
597599
if toolsResult.IsArray() {
598-
toolsJSON = []byte(`[{"functionDeclarations":[]}]`)
600+
functionToolNode := []byte(`{"functionDeclarations":[]}`)
601+
googleSearchNodes := make([][]byte, 0)
599602
toolsResults := toolsResult.Array()
600603
for i := 0; i < len(toolsResults); i++ {
601604
toolResult := toolsResults[i]
605+
if isClaudeTypedWebSearchToolType(toolResult.Get("type").String()) && googleSearchModelName != "" {
606+
googleSearchNodes = append(googleSearchNodes, []byte(`{"googleSearch":{}}`))
607+
googleSearchToolCount++
608+
continue
609+
}
602610
inputSchemaResult := toolResult.Get("input_schema")
603611
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
604612
// Sanitize the input schema for Antigravity API compatibility
@@ -612,18 +620,31 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
612620
}
613621
tool, _ = sjson.DeleteBytes(tool, toolKey)
614622
}
615-
toolsJSON, _ = sjson.SetRawBytes(toolsJSON, "0.functionDeclarations.-1", tool)
623+
functionToolNode, _ = sjson.SetRawBytes(functionToolNode, "functionDeclarations.-1", tool)
616624
toolDeclCount++
617625
}
618626
}
627+
if toolDeclCount > 0 || len(googleSearchNodes) > 0 {
628+
toolsJSON = []byte(`[]`)
629+
if toolDeclCount > 0 {
630+
toolsJSON, _ = sjson.SetRawBytes(toolsJSON, "-1", functionToolNode)
631+
}
632+
for _, googleSearchNode := range googleSearchNodes {
633+
toolsJSON, _ = sjson.SetRawBytes(toolsJSON, "-1", googleSearchNode)
634+
}
635+
}
619636
}
620637

621638
// Build output Gemini CLI request JSON
622639
out := []byte(`{"model":"","request":{"contents":[]}}`)
623-
out, _ = sjson.SetBytes(out, "model", modelName)
640+
outputModelName := modelName
641+
if googleSearchToolCount > 0 {
642+
outputModelName = googleSearchModelName
643+
}
644+
out, _ = sjson.SetBytes(out, "model", outputModelName)
624645

625646
// Inject interleaved thinking hint when both tools and thinking are active
626-
hasTools := toolDeclCount > 0
647+
hasTools := toolDeclCount > 0 || googleSearchToolCount > 0
627648
thinkingResult := gjson.GetBytes(rawJSON, "thinking")
628649
thinkingType := thinkingResult.Get("type").String()
629650
hasThinking := thinkingResult.Exists() && thinkingResult.IsObject() && (thinkingType == "enabled" || thinkingType == "adaptive" || thinkingType == "auto")
@@ -653,7 +674,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
653674
if hasContents {
654675
out, _ = sjson.SetRawBytes(out, "request.contents", contentsJSON)
655676
}
656-
if toolDeclCount > 0 {
677+
if toolDeclCount > 0 || googleSearchToolCount > 0 {
657678
out, _ = sjson.SetRawBytes(out, "request.tools", toolsJSON)
658679
}
659680

0 commit comments

Comments
 (0)