fix: expose mapped antigravity test models

This commit is contained in:
yJader
2026-10-03 20:56:49 +08:00
parent b8dece9000
commit 856d7ea905
4 changed files with 96 additions and 3 deletions
+4
View File
@@ -876,6 +876,10 @@ Antigravity accounts support optional **hybrid scheduling**. When enabled, the g
> **⚠️ Warning**: Anthropic Claude and Antigravity Claude **cannot be mixed within the same conversation context**. Use groups to isolate them properly.
### Admin Account Test Models
For Antigravity accounts, the connectivity test model selector uses the request-side keys of an explicitly configured account `model_mapping`. If the mapping is missing or empty, it uses the built-in model catalog. The selected key is still mapped to its upstream value when the test runs.
---
## Project Structure
+4
View File
@@ -792,6 +792,10 @@ Antigravity 账户支持可选的**混合调度**功能。开启后,通用端
> **⚠️ 注意**:Anthropic Claude 和 Antigravity Claude **不能在同一上下文中混合使用**,请通过分组功能做好隔离。
### 管理端账号测试模型
在管理端打开“测试账号连接”时,Antigravity 账户的可选测试模型遵循账号映射配置:配置了账号级 `model_mapping` 时,列表使用映射的请求侧 key;未配置或映射为空时,列表使用内置默认模型目录。选择映射 key 后,测试请求仍会按映射 value 转换为实际发送给上游的模型。
---
## 项目结构
@@ -2894,10 +2894,9 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
return
}
// Handle Antigravity accounts: return Claude + Gemini models
// Explicit account mappings expose their request-side names to connectivity tests.
if account.Platform == service.PlatformAntigravity {
// 直接复用 antigravity.DefaultModels(),与 /v1/models 端点保持同步
response.Success(c, antigravity.DefaultModels())
response.Success(c, antigravityAccountTestModels(account.Credentials["model_mapping"]))
return
}
@@ -2998,6 +2997,48 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
response.Success(c, models)
}
// antigravityAccountTestModels uses the stored mapping rather than GetModelMapping,
// which supplies defaults and compatibility aliases that the administrator did not configure.
func antigravityAccountTestModels(rawMapping any) []antigravity.ClaudeModel {
var mappedIDs []string
switch mapping := rawMapping.(type) {
case map[string]any:
for id := range mapping {
if strings.TrimSpace(id) != "" {
mappedIDs = append(mappedIDs, id)
}
}
case map[string]string:
for id := range mapping {
if strings.TrimSpace(id) != "" {
mappedIDs = append(mappedIDs, id)
}
}
}
if len(mappedIDs) == 0 {
return antigravity.DefaultModels()
}
sort.Strings(mappedIDs)
defaultByID := make(map[string]antigravity.ClaudeModel)
for _, model := range antigravity.DefaultModels() {
defaultByID[model.ID] = model
}
models := make([]antigravity.ClaudeModel, 0, len(mappedIDs))
for _, id := range mappedIDs {
if model, ok := defaultByID[id]; ok {
models = append(models, model)
continue
}
models = append(models, antigravity.ClaudeModel{
ID: id,
Type: "model",
DisplayName: id,
})
}
return models
}
// SyncUpstreamModels handles syncing live supported models from an account's upstream.
// POST /api/v1/admin/accounts/:id/models/sync-upstream
func (h *AccountHandler) SyncUpstreamModels(c *gin.Context) {
@@ -10,6 +10,7 @@ import (
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
@@ -330,6 +331,49 @@ func TestAccountHandlerGetAvailableModels_GeminiGoogleOneUsesConservativeCatalog
require.NotContains(t, ids, "gemini-2.5-flash-image")
}
func TestAccountHandlerGetAvailableModels_Antigravity(t *testing.T) {
defaults := antigravity.DefaultModels()
known := defaults[0]
custom := antigravity.ClaudeModel{ID: "claude-custom-alias", Type: "model", DisplayName: "claude-custom-alias"}
cases := []struct {
name string
credentials map[string]any
want []antigravity.ClaudeModel
}{
{name: "nil credentials", want: defaults},
{name: "missing mapping", credentials: map[string]any{}, want: defaults},
{name: "empty JSON mapping", credentials: map[string]any{"model_mapping": map[string]any{}}, want: defaults},
{name: "empty string mapping", credentials: map[string]any{"model_mapping": map[string]string{}}, want: defaults},
{name: "blank key", credentials: map[string]any{"model_mapping": map[string]any{" ": "upstream-model"}}, want: defaults},
{name: "JSON mapping keys and metadata", credentials: map[string]any{"model_mapping": map[string]any{
known.ID: known.ID, custom.ID: "upstream-model",
}}, want: []antigravity.ClaudeModel{custom, known}},
{name: "string mapping keys and metadata", credentials: map[string]any{"model_mapping": map[string]string{
known.ID: known.ID, custom.ID: "upstream-model",
}}, want: []antigravity.ClaudeModel{custom, known}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
for _, accountType := range []string{service.AccountTypeOAuth, service.AccountTypeAPIKey} {
t.Run(accountType, func(t *testing.T) {
svc := &availableModelsAdminService{
stubAdminService: newStubAdminService(),
account: service.Account{ID: 51, Platform: service.PlatformAntigravity, Type: accountType, Credentials: tc.credentials},
}
rec := httptest.NewRecorder()
setupAvailableModelsRouter(svc).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/51/models", nil))
require.Equal(t, http.StatusOK, rec.Code)
var resp struct {
Data []antigravity.ClaudeModel `json:"data"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
require.Equal(t, tc.want, resp.Data)
})
}
})
}
}
func TestAccountHandlerSyncUpstreamModels_ConfigErrorReturnsBadRequest(t *testing.T) {
svc := &availableModelsAdminService{
stubAdminService: newStubAdminService(),