Merge pull request #2082 from router-for-me/antigravity

Refactor Antigravity model handling and improve logging
This commit is contained in:
Luis Pater
2026-03-12 10:39:13 +08:00
committed by GitHub
8 changed files with 410 additions and 646 deletions

View File

@@ -3,32 +3,24 @@
package registry
import (
"sort"
"strings"
)
// AntigravityModelConfig captures static antigravity model overrides, including
// Thinking budget limits and provider max completion tokens.
type AntigravityModelConfig struct {
Thinking *ThinkingSupport `json:"thinking,omitempty"`
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"`
}
// staticModelsJSON mirrors the top-level structure of models.json.
type staticModelsJSON struct {
Claude []*ModelInfo `json:"claude"`
Gemini []*ModelInfo `json:"gemini"`
Vertex []*ModelInfo `json:"vertex"`
GeminiCLI []*ModelInfo `json:"gemini-cli"`
AIStudio []*ModelInfo `json:"aistudio"`
CodexFree []*ModelInfo `json:"codex-free"`
CodexTeam []*ModelInfo `json:"codex-team"`
CodexPlus []*ModelInfo `json:"codex-plus"`
CodexPro []*ModelInfo `json:"codex-pro"`
Qwen []*ModelInfo `json:"qwen"`
IFlow []*ModelInfo `json:"iflow"`
Kimi []*ModelInfo `json:"kimi"`
Antigravity map[string]*AntigravityModelConfig `json:"antigravity"`
Claude []*ModelInfo `json:"claude"`
Gemini []*ModelInfo `json:"gemini"`
Vertex []*ModelInfo `json:"vertex"`
GeminiCLI []*ModelInfo `json:"gemini-cli"`
AIStudio []*ModelInfo `json:"aistudio"`
CodexFree []*ModelInfo `json:"codex-free"`
CodexTeam []*ModelInfo `json:"codex-team"`
CodexPlus []*ModelInfo `json:"codex-plus"`
CodexPro []*ModelInfo `json:"codex-pro"`
Qwen []*ModelInfo `json:"qwen"`
IFlow []*ModelInfo `json:"iflow"`
Kimi []*ModelInfo `json:"kimi"`
Antigravity []*ModelInfo `json:"antigravity"`
}
// GetClaudeModels returns the standard Claude model definitions.
@@ -91,33 +83,9 @@ func GetKimiModels() []*ModelInfo {
return cloneModelInfos(getModels().Kimi)
}
// GetAntigravityModelConfig returns static configuration for antigravity models.
// Keys use upstream model names returned by the Antigravity models endpoint.
func GetAntigravityModelConfig() map[string]*AntigravityModelConfig {
data := getModels()
if len(data.Antigravity) == 0 {
return nil
}
out := make(map[string]*AntigravityModelConfig, len(data.Antigravity))
for k, v := range data.Antigravity {
out[k] = cloneAntigravityModelConfig(v)
}
return out
}
func cloneAntigravityModelConfig(cfg *AntigravityModelConfig) *AntigravityModelConfig {
if cfg == nil {
return nil
}
copyConfig := *cfg
if cfg.Thinking != nil {
copyThinking := *cfg.Thinking
if len(cfg.Thinking.Levels) > 0 {
copyThinking.Levels = append([]string(nil), cfg.Thinking.Levels...)
}
copyConfig.Thinking = &copyThinking
}
return &copyConfig
// GetAntigravityModels returns the standard Antigravity model definitions.
func GetAntigravityModels() []*ModelInfo {
return cloneModelInfos(getModels().Antigravity)
}
// cloneModelInfos returns a shallow copy of the slice with each element deep-cloned.
@@ -145,7 +113,7 @@ func cloneModelInfos(models []*ModelInfo) []*ModelInfo {
// - qwen
// - iflow
// - kimi
// - antigravity (returns static overrides only)
// - antigravity
func GetStaticModelDefinitionsByChannel(channel string) []*ModelInfo {
key := strings.ToLower(strings.TrimSpace(channel))
switch key {
@@ -168,28 +136,7 @@ func GetStaticModelDefinitionsByChannel(channel string) []*ModelInfo {
case "kimi":
return GetKimiModels()
case "antigravity":
cfg := GetAntigravityModelConfig()
if len(cfg) == 0 {
return nil
}
models := make([]*ModelInfo, 0, len(cfg))
for modelID, entry := range cfg {
if modelID == "" || entry == nil {
continue
}
models = append(models, &ModelInfo{
ID: modelID,
Object: "model",
OwnedBy: "antigravity",
Type: "antigravity",
Thinking: entry.Thinking,
MaxCompletionTokens: entry.MaxCompletionTokens,
})
}
sort.Slice(models, func(i, j int) bool {
return strings.ToLower(models[i].ID) < strings.ToLower(models[j].ID)
})
return models
return GetAntigravityModels()
default:
return nil
}
@@ -213,6 +160,7 @@ func LookupStaticModelInfo(modelID string) *ModelInfo {
data.Qwen,
data.IFlow,
data.Kimi,
data.Antigravity,
}
for _, models := range allModels {
for _, m := range models {
@@ -222,14 +170,5 @@ func LookupStaticModelInfo(modelID string) *ModelInfo {
}
}
// Check Antigravity static config
if cfg := cloneAntigravityModelConfig(data.Antigravity[modelID]); cfg != nil {
return &ModelInfo{
ID: modelID,
Thinking: cfg.Thinking,
MaxCompletionTokens: cfg.MaxCompletionTokens,
}
}
return nil
}

View File

@@ -100,7 +100,7 @@ func tryRefreshModels(ctx context.Context) {
log.Infof("models updated from %s", url)
return
}
log.Warn("models refresh failed from all URLs, using current data")
log.Warn("models refresh failed from all URLs, using local data")
}
func loadModelsFromBytes(data []byte, source string) error {
@@ -145,6 +145,7 @@ func validateModelsCatalog(data *staticModelsJSON) error {
{name: "qwen", models: data.Qwen},
{name: "iflow", models: data.IFlow},
{name: "kimi", models: data.Kimi},
{name: "antigravity", models: data.Antigravity},
}
for _, section := range requiredSections {
@@ -152,9 +153,6 @@ func validateModelsCatalog(data *staticModelsJSON) error {
return err
}
}
if err := validateAntigravitySection(data.Antigravity); err != nil {
return err
}
return nil
}
@@ -179,20 +177,3 @@ func validateModelSection(section string, models []*ModelInfo) error {
}
return nil
}
func validateAntigravitySection(configs map[string]*AntigravityModelConfig) error {
if len(configs) == 0 {
return fmt.Errorf("antigravity section is empty")
}
for modelID, cfg := range configs {
trimmedID := strings.TrimSpace(modelID)
if trimmedID == "" {
return fmt.Errorf("antigravity contains empty model id")
}
if cfg == nil {
return fmt.Errorf("antigravity[%q] is null", trimmedID)
}
}
return nil
}

View File

@@ -2481,40 +2481,83 @@
}
}
],
"antigravity": {
"claude-opus-4-6-thinking": {
"antigravity": [
{
"id": "claude-opus-4-6-thinking",
"object": "model",
"owned_by": "antigravity",
"type": "antigravity",
"display_name": "Claude Opus 4.6 (Thinking)",
"name": "claude-opus-4-6-thinking",
"description": "Claude Opus 4.6 (Thinking)",
"context_length": 200000,
"max_completion_tokens": 64000,
"thinking": {
"min": 1024,
"max": 64000,
"zero_allowed": true,
"dynamic_allowed": true
},
"max_completion_tokens": 64000
}
},
"claude-sonnet-4-6": {
{
"id": "claude-sonnet-4-6",
"object": "model",
"owned_by": "antigravity",
"type": "antigravity",
"display_name": "Claude Sonnet 4.6 (Thinking)",
"name": "claude-sonnet-4-6",
"description": "Claude Sonnet 4.6 (Thinking)",
"context_length": 200000,
"max_completion_tokens": 64000,
"thinking": {
"min": 1024,
"max": 64000,
"zero_allowed": true,
"dynamic_allowed": true
},
"max_completion_tokens": 64000
}
},
"gemini-2.5-flash": {
{
"id": "gemini-2.5-flash",
"object": "model",
"owned_by": "antigravity",
"type": "antigravity",
"display_name": "Gemini 2.5 Flash",
"name": "gemini-2.5-flash",
"description": "Gemini 2.5 Flash",
"context_length": 1048576,
"max_completion_tokens": 65535,
"thinking": {
"max": 24576,
"zero_allowed": true,
"dynamic_allowed": true
}
},
"gemini-2.5-flash-lite": {
{
"id": "gemini-2.5-flash-lite",
"object": "model",
"owned_by": "antigravity",
"type": "antigravity",
"display_name": "Gemini 2.5 Flash Lite",
"name": "gemini-2.5-flash-lite",
"description": "Gemini 2.5 Flash Lite",
"context_length": 1048576,
"max_completion_tokens": 65535,
"thinking": {
"max": 24576,
"zero_allowed": true,
"dynamic_allowed": true
}
},
"gemini-3-flash": {
{
"id": "gemini-3-flash",
"object": "model",
"owned_by": "antigravity",
"type": "antigravity",
"display_name": "Gemini 3 Flash",
"name": "gemini-3-flash",
"description": "Gemini 3 Flash",
"context_length": 1048576,
"max_completion_tokens": 65536,
"thinking": {
"min": 128,
"max": 32768,
@@ -2527,7 +2570,16 @@
]
}
},
"gemini-3-pro-high": {
{
"id": "gemini-3-pro-high",
"object": "model",
"owned_by": "antigravity",
"type": "antigravity",
"display_name": "Gemini 3 Pro (High)",
"name": "gemini-3-pro-high",
"description": "Gemini 3 Pro (High)",
"context_length": 1048576,
"max_completion_tokens": 65535,
"thinking": {
"min": 128,
"max": 32768,
@@ -2538,7 +2590,16 @@
]
}
},
"gemini-3-pro-low": {
{
"id": "gemini-3-pro-low",
"object": "model",
"owned_by": "antigravity",
"type": "antigravity",
"display_name": "Gemini 3 Pro (Low)",
"name": "gemini-3-pro-low",
"description": "Gemini 3 Pro (Low)",
"context_length": 1048576,
"max_completion_tokens": 65535,
"thinking": {
"min": 128,
"max": 32768,
@@ -2549,7 +2610,14 @@
]
}
},
"gemini-3.1-flash-image": {
{
"id": "gemini-3.1-flash-image",
"object": "model",
"owned_by": "antigravity",
"type": "antigravity",
"display_name": "Gemini 3.1 Flash Image",
"name": "gemini-3.1-flash-image",
"description": "Gemini 3.1 Flash Image",
"thinking": {
"min": 128,
"max": 32768,
@@ -2560,18 +2628,16 @@
]
}
},
"gemini-3.1-flash-lite-preview": {
"thinking": {
"min": 128,
"max": 32768,
"dynamic_allowed": true,
"levels": [
"minimal",
"high"
]
}
},
"gemini-3.1-pro-high": {
{
"id": "gemini-3.1-pro-high",
"object": "model",
"owned_by": "antigravity",
"type": "antigravity",
"display_name": "Gemini 3.1 Pro (High)",
"name": "gemini-3.1-pro-high",
"description": "Gemini 3.1 Pro (High)",
"context_length": 1048576,
"max_completion_tokens": 65535,
"thinking": {
"min": 128,
"max": 32768,
@@ -2582,7 +2648,16 @@
]
}
},
"gemini-3.1-pro-low": {
{
"id": "gemini-3.1-pro-low",
"object": "model",
"owned_by": "antigravity",
"type": "antigravity",
"display_name": "Gemini 3.1 Pro (Low)",
"name": "gemini-3.1-pro-low",
"description": "Gemini 3.1 Pro (Low)",
"context_length": 1048576,
"max_completion_tokens": 65535,
"thinking": {
"min": 128,
"max": 32768,
@@ -2593,6 +2668,16 @@
]
}
},
"gpt-oss-120b-medium": {}
}
{
"id": "gpt-oss-120b-medium",
"object": "model",
"owned_by": "antigravity",
"type": "antigravity",
"display_name": "GPT-OSS 120B (Medium)",
"name": "gpt-oss-120b-medium",
"description": "GPT-OSS 120B (Medium)",
"context_length": 114000,
"max_completion_tokens": 32768
}
]
}

View File

@@ -24,7 +24,6 @@ import (
"github.com/google/uuid"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
@@ -43,7 +42,6 @@ const (
antigravityCountTokensPath = "/v1internal:countTokens"
antigravityStreamPath = "/v1internal:streamGenerateContent"
antigravityGeneratePath = "/v1internal:generateContent"
antigravityModelsPath = "/v1internal:fetchAvailableModels"
antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
antigravityClientSecret = "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"
defaultAntigravityAgent = "antigravity/1.19.6 darwin/arm64"
@@ -55,78 +53,8 @@ const (
var (
randSource = rand.New(rand.NewSource(time.Now().UnixNano()))
randSourceMutex sync.Mutex
// antigravityPrimaryModelsCache keeps the latest non-empty model list fetched
// from any antigravity auth. Empty fetches never overwrite this cache.
antigravityPrimaryModelsCache struct {
mu sync.RWMutex
models []*registry.ModelInfo
}
)
func cloneAntigravityModels(models []*registry.ModelInfo) []*registry.ModelInfo {
if len(models) == 0 {
return nil
}
out := make([]*registry.ModelInfo, 0, len(models))
for _, model := range models {
if model == nil || strings.TrimSpace(model.ID) == "" {
continue
}
out = append(out, cloneAntigravityModelInfo(model))
}
if len(out) == 0 {
return nil
}
return out
}
func cloneAntigravityModelInfo(model *registry.ModelInfo) *registry.ModelInfo {
if model == nil {
return nil
}
clone := *model
if len(model.SupportedGenerationMethods) > 0 {
clone.SupportedGenerationMethods = append([]string(nil), model.SupportedGenerationMethods...)
}
if len(model.SupportedParameters) > 0 {
clone.SupportedParameters = append([]string(nil), model.SupportedParameters...)
}
if model.Thinking != nil {
thinkingClone := *model.Thinking
if len(model.Thinking.Levels) > 0 {
thinkingClone.Levels = append([]string(nil), model.Thinking.Levels...)
}
clone.Thinking = &thinkingClone
}
return &clone
}
func storeAntigravityPrimaryModels(models []*registry.ModelInfo) bool {
cloned := cloneAntigravityModels(models)
if len(cloned) == 0 {
return false
}
antigravityPrimaryModelsCache.mu.Lock()
antigravityPrimaryModelsCache.models = cloned
antigravityPrimaryModelsCache.mu.Unlock()
return true
}
func loadAntigravityPrimaryModels() []*registry.ModelInfo {
antigravityPrimaryModelsCache.mu.RLock()
cloned := cloneAntigravityModels(antigravityPrimaryModelsCache.models)
antigravityPrimaryModelsCache.mu.RUnlock()
return cloned
}
func fallbackAntigravityPrimaryModels() []*registry.ModelInfo {
models := loadAntigravityPrimaryModels()
if len(models) > 0 {
log.Debugf("antigravity executor: using cached primary model list (%d models)", len(models))
}
return models
}
// AntigravityExecutor proxies requests to the antigravity upstream.
type AntigravityExecutor struct {
cfg *config.Config
@@ -1150,168 +1078,6 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
}
}
// FetchAntigravityModels retrieves available models using the supplied auth.
func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *config.Config) []*registry.ModelInfo {
exec := &AntigravityExecutor{cfg: cfg}
token, updatedAuth, errToken := exec.ensureAccessToken(ctx, auth)
if errToken != nil || token == "" {
return fallbackAntigravityPrimaryModels()
}
if updatedAuth != nil {
auth = updatedAuth
}
baseURLs := antigravityBaseURLFallbackOrder(auth)
httpClient := newAntigravityHTTPClient(ctx, cfg, auth, 0)
for idx, baseURL := range baseURLs {
modelsURL := baseURL + antigravityModelsPath
var payload []byte
if auth != nil && auth.Metadata != nil {
if pid, ok := auth.Metadata["project_id"].(string); ok && strings.TrimSpace(pid) != "" {
payload = []byte(fmt.Sprintf(`{"project": "%s"}`, strings.TrimSpace(pid)))
}
}
if len(payload) == 0 {
payload = []byte(`{}`)
}
httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, modelsURL, bytes.NewReader(payload))
if errReq != nil {
return fallbackAntigravityPrimaryModels()
}
httpReq.Close = true
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
httpReq.Header.Set("User-Agent", resolveUserAgent(auth))
if host := resolveHost(baseURL); host != "" {
httpReq.Host = host
}
httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil {
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
return fallbackAntigravityPrimaryModels()
}
if idx+1 < len(baseURLs) {
log.Debugf("antigravity executor: models request error on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1])
continue
}
return fallbackAntigravityPrimaryModels()
}
bodyBytes, errRead := io.ReadAll(httpResp.Body)
if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("antigravity executor: close response body error: %v", errClose)
}
if errRead != nil {
if idx+1 < len(baseURLs) {
log.Debugf("antigravity executor: models read error on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1])
continue
}
return fallbackAntigravityPrimaryModels()
}
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
if httpResp.StatusCode == http.StatusTooManyRequests && idx+1 < len(baseURLs) {
log.Debugf("antigravity executor: models request rate limited on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1])
continue
}
if idx+1 < len(baseURLs) {
log.Debugf("antigravity executor: models request failed with status %d on base url %s, retrying with fallback base url: %s", httpResp.StatusCode, baseURL, baseURLs[idx+1])
continue
}
return fallbackAntigravityPrimaryModels()
}
result := gjson.GetBytes(bodyBytes, "models")
if !result.Exists() {
if idx+1 < len(baseURLs) {
log.Debugf("antigravity executor: models field missing on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1])
continue
}
return fallbackAntigravityPrimaryModels()
}
now := time.Now().Unix()
modelConfig := registry.GetAntigravityModelConfig()
models := make([]*registry.ModelInfo, 0, len(result.Map()))
for originalName, modelData := range result.Map() {
modelID := strings.TrimSpace(originalName)
if modelID == "" {
continue
}
switch modelID {
case "chat_20706", "chat_23310", "tab_flash_lite_preview", "tab_jump_flash_lite_preview", "gemini-2.5-flash-thinking", "gemini-2.5-pro":
continue
}
modelCfg := modelConfig[modelID]
// Extract displayName from upstream response, fallback to modelID
displayName := modelData.Get("displayName").String()
if displayName == "" {
displayName = modelID
}
modelInfo := &registry.ModelInfo{
ID: modelID,
Name: modelID,
Description: displayName,
DisplayName: displayName,
Version: modelID,
Object: "model",
Created: now,
OwnedBy: antigravityAuthType,
Type: antigravityAuthType,
}
// Build input modalities from upstream capability flags.
inputModalities := []string{"TEXT"}
if modelData.Get("supportsImages").Bool() {
inputModalities = append(inputModalities, "IMAGE")
}
if modelData.Get("supportsVideo").Bool() {
inputModalities = append(inputModalities, "VIDEO")
}
modelInfo.SupportedInputModalities = inputModalities
modelInfo.SupportedOutputModalities = []string{"TEXT"}
// Token limits from upstream.
if maxTok := modelData.Get("maxTokens").Int(); maxTok > 0 {
modelInfo.InputTokenLimit = int(maxTok)
}
if maxOut := modelData.Get("maxOutputTokens").Int(); maxOut > 0 {
modelInfo.OutputTokenLimit = int(maxOut)
}
// Supported generation methods (Gemini v1beta convention).
modelInfo.SupportedGenerationMethods = []string{"generateContent", "countTokens"}
// Look up Thinking support from static config using upstream model name.
if modelCfg != nil {
if modelCfg.Thinking != nil {
modelInfo.Thinking = modelCfg.Thinking
}
if modelCfg.MaxCompletionTokens > 0 {
modelInfo.MaxCompletionTokens = modelCfg.MaxCompletionTokens
}
}
models = append(models, modelInfo)
}
if len(models) == 0 {
if idx+1 < len(baseURLs) {
log.Debugf("antigravity executor: empty models list on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1])
continue
}
log.Debug("antigravity executor: fetched empty model list; retaining cached primary model list")
return fallbackAntigravityPrimaryModels()
}
storeAntigravityPrimaryModels(models)
return models
}
return fallbackAntigravityPrimaryModels()
}
func (e *AntigravityExecutor) ensureAccessToken(ctx context.Context, auth *cliproxyauth.Auth) (string, *cliproxyauth.Auth, error) {
if auth == nil {
return "", nil, statusErr{code: http.StatusUnauthorized, msg: "missing auth"}

View File

@@ -1,90 +0,0 @@
package executor
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
)
func resetAntigravityPrimaryModelsCacheForTest() {
antigravityPrimaryModelsCache.mu.Lock()
antigravityPrimaryModelsCache.models = nil
antigravityPrimaryModelsCache.mu.Unlock()
}
func TestStoreAntigravityPrimaryModels_EmptyDoesNotOverwrite(t *testing.T) {
resetAntigravityPrimaryModelsCacheForTest()
t.Cleanup(resetAntigravityPrimaryModelsCacheForTest)
seed := []*registry.ModelInfo{
{ID: "claude-sonnet-4-5"},
{ID: "gemini-2.5-pro"},
}
if updated := storeAntigravityPrimaryModels(seed); !updated {
t.Fatal("expected non-empty model list to update primary cache")
}
if updated := storeAntigravityPrimaryModels(nil); updated {
t.Fatal("expected nil model list not to overwrite primary cache")
}
if updated := storeAntigravityPrimaryModels([]*registry.ModelInfo{}); updated {
t.Fatal("expected empty model list not to overwrite primary cache")
}
got := loadAntigravityPrimaryModels()
if len(got) != 2 {
t.Fatalf("expected cached model count 2, got %d", len(got))
}
if got[0].ID != "claude-sonnet-4-5" || got[1].ID != "gemini-2.5-pro" {
t.Fatalf("unexpected cached model ids: %q, %q", got[0].ID, got[1].ID)
}
}
func TestLoadAntigravityPrimaryModels_ReturnsClone(t *testing.T) {
resetAntigravityPrimaryModelsCacheForTest()
t.Cleanup(resetAntigravityPrimaryModelsCacheForTest)
if updated := storeAntigravityPrimaryModels([]*registry.ModelInfo{{
ID: "gpt-5",
DisplayName: "GPT-5",
SupportedGenerationMethods: []string{"generateContent"},
SupportedParameters: []string{"temperature"},
Thinking: &registry.ThinkingSupport{
Levels: []string{"high"},
},
}}); !updated {
t.Fatal("expected model cache update")
}
got := loadAntigravityPrimaryModels()
if len(got) != 1 {
t.Fatalf("expected one cached model, got %d", len(got))
}
got[0].ID = "mutated-id"
if len(got[0].SupportedGenerationMethods) > 0 {
got[0].SupportedGenerationMethods[0] = "mutated-method"
}
if len(got[0].SupportedParameters) > 0 {
got[0].SupportedParameters[0] = "mutated-parameter"
}
if got[0].Thinking != nil && len(got[0].Thinking.Levels) > 0 {
got[0].Thinking.Levels[0] = "mutated-level"
}
again := loadAntigravityPrimaryModels()
if len(again) != 1 {
t.Fatalf("expected one cached model after mutation, got %d", len(again))
}
if again[0].ID != "gpt-5" {
t.Fatalf("expected cached model id to remain %q, got %q", "gpt-5", again[0].ID)
}
if len(again[0].SupportedGenerationMethods) == 0 || again[0].SupportedGenerationMethods[0] != "generateContent" {
t.Fatalf("expected cached generation methods to be unmutated, got %v", again[0].SupportedGenerationMethods)
}
if len(again[0].SupportedParameters) == 0 || again[0].SupportedParameters[0] != "temperature" {
t.Fatalf("expected cached supported parameters to be unmutated, got %v", again[0].SupportedParameters)
}
if again[0].Thinking == nil || len(again[0].Thinking.Levels) == 0 || again[0].Thinking.Levels[0] != "high" {
t.Fatalf("expected cached model thinking levels to be unmutated, got %v", again[0].Thinking)
}
}