mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-07 16:53:24 +00:00
fix(gemini): honor custom native models list
This commit is contained in:
1 parent
b1748c4ea9
commit
28f673e4cd
6 files changed
+106
-8
No files matched your search
@@ -52,6 +52,11 @@ func (h *GatewayHandler) GeminiV1BetaListModels(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if models, ok := customGeminiModelsList(apiKey.Group); ok {
|
||||||
|
c.JSON(http.StatusOK, models)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
account, err := h.geminiCompatService.SelectAccountForAIStudioEndpoints(c.Request.Context(), apiKey.GroupID)
|
account, err := h.geminiCompatService.SelectAccountForAIStudioEndpoints(c.Request.Context(), apiKey.GroupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 没有 gemini 账户,检查是否有 antigravity 账户可用
|
// 没有 gemini 账户,检查是否有 antigravity 账户可用
|
||||||
@@ -78,6 +83,17 @@ func (h *GatewayHandler) GeminiV1BetaListModels(c *gin.Context) {
|
|||||||
writeUpstreamResponse(c, res)
|
writeUpstreamResponse(c, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func customGeminiModelsList(group *service.Group) (gemini.ModelsListResponse, bool) {
|
||||||
|
if group == nil || !group.CustomModelsListEnabled() {
|
||||||
|
return gemini.ModelsListResponse{}, false
|
||||||
|
}
|
||||||
|
models := make([]gemini.Model, 0, len(group.ModelsListConfig.Models))
|
||||||
|
for _, modelID := range group.ModelsListConfig.Models {
|
||||||
|
models = append(models, gemini.FallbackModel(modelID))
|
||||||
|
}
|
||||||
|
return gemini.ModelsListResponse{Models: models}, true
|
||||||
|
}
|
||||||
|
|
||||||
// GeminiV1BetaGetModel proxies:
|
// GeminiV1BetaGetModel proxies:
|
||||||
// GET /v1beta/models/{model}
|
// GET /v1beta/models/{model}
|
||||||
func (h *GatewayHandler) GeminiV1BetaGetModel(c *gin.Context) {
|
func (h *GatewayHandler) GeminiV1BetaGetModel(c *gin.Context) {
|
||||||
|
|||||||
@@ -3,13 +3,81 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/gemini"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestGeminiV1BetaListModels_CustomGroupListUsesNativeResponse(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(rec)
|
||||||
|
c.Request = httptest.NewRequest(http.MethodGet, "/v1beta/models", nil)
|
||||||
|
c.Set(string(middleware.ContextKeyAPIKey), &service.APIKey{
|
||||||
|
Group: &service.Group{
|
||||||
|
Platform: service.PlatformGemini,
|
||||||
|
ModelsListConfig: service.GroupModelsListConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Models: []string{"gemini-2.5-pro", "models/gemini-custom"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
(&GatewayHandler{}).GeminiV1BetaListModels(c)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
var got gemini.ModelsListResponse
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
||||||
|
require.Equal(t, []gemini.Model{
|
||||||
|
gemini.FallbackModel("gemini-2.5-pro"),
|
||||||
|
gemini.FallbackModel("models/gemini-custom"),
|
||||||
|
}, got.Models)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeminiV1BetaListModels_ForcedAntigravityIgnoresCustomGroupList(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(rec)
|
||||||
|
c.Request = httptest.NewRequest(http.MethodGet, "/antigravity/v1beta/models", nil)
|
||||||
|
c.Set(string(middleware.ContextKeyAPIKey), &service.APIKey{
|
||||||
|
Group: &service.Group{
|
||||||
|
Platform: service.PlatformGemini,
|
||||||
|
ModelsListConfig: service.GroupModelsListConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Models: []string{"gemini-custom"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
c.Set(string(middleware.ContextKeyForcePlatform), service.PlatformAntigravity)
|
||||||
|
|
||||||
|
(&GatewayHandler{}).GeminiV1BetaListModels(c)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
var got antigravity.GeminiModelsListResponse
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
||||||
|
require.Equal(t, antigravity.FallbackGeminiModelsList(), got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomGeminiModelsList_DisabledKeepsExistingFlow(t *testing.T) {
|
||||||
|
group := &service.Group{
|
||||||
|
ModelsListConfig: service.GroupModelsListConfig{
|
||||||
|
Enabled: false,
|
||||||
|
Models: []string{"gemini-2.5-pro"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := customGeminiModelsList(group)
|
||||||
|
require.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
// TestGeminiV1BetaHandler_PlatformRoutingInvariant 文档化并验证 Handler 层的平台路由逻辑不变量
|
// TestGeminiV1BetaHandler_PlatformRoutingInvariant 文档化并验证 Handler 层的平台路由逻辑不变量
|
||||||
// 该测试确保 gemini 和 antigravity 平台的路由逻辑符合预期
|
// 该测试确保 gemini 和 antigravity 平台的路由逻辑符合预期
|
||||||
func TestGeminiV1BetaHandler_PlatformRoutingInvariant(t *testing.T) {
|
func TestGeminiV1BetaHandler_PlatformRoutingInvariant(t *testing.T) {
|
||||||
|
|||||||
@@ -1070,8 +1070,8 @@ export default {
|
|||||||
sumTooHigh: 'Min gross margin plus safety buffer must be less than 100%, otherwise every account would be excluded'
|
sumTooHigh: 'Min gross margin plus safety buffer must be less than 100%, otherwise every account would be excluded'
|
||||||
},
|
},
|
||||||
modelsList: {
|
modelsList: {
|
||||||
title: 'Custom /v1/models Model List',
|
title: 'Custom {endpoint} Model List',
|
||||||
hint: 'Only changes the /v1/models response. Whitelist model calls and account routing are unchanged.',
|
hint: 'Only changes the {endpoint} response. Whitelist model calls and account routing are unchanged.',
|
||||||
loading: 'Loading model list...',
|
loading: 'Loading model list...',
|
||||||
empty: 'No displayable models',
|
empty: 'No displayable models',
|
||||||
selectedSummary: 'Selected {selected} / {total}',
|
selectedSummary: 'Selected {selected} / {total}',
|
||||||
|
|||||||
@@ -1067,8 +1067,8 @@ export default {
|
|||||||
sumTooHigh: '最低毛利率与安全缓冲之和必须小于 100%,否则将排除全部账号'
|
sumTooHigh: '最低毛利率与安全缓冲之和必须小于 100%,否则将排除全部账号'
|
||||||
},
|
},
|
||||||
modelsList: {
|
modelsList: {
|
||||||
title: '自定义 /v1/models 模型列表',
|
title: '自定义 {endpoint} 模型列表',
|
||||||
hint: '仅影响 /v1/models 展示结果,不影响白名单模型调用和账号调度。',
|
hint: '仅影响 {endpoint} 展示结果,不影响白名单模型调用和账号调度。',
|
||||||
loading: '正在加载模型列表...',
|
loading: '正在加载模型列表...',
|
||||||
empty: '暂无可展示模型',
|
empty: '暂无可展示模型',
|
||||||
selectedSummary: '已选 {selected} / {total}',
|
selectedSummary: '已选 {selected} / {total}',
|
||||||
|
|||||||
@@ -775,10 +775,10 @@
|
|||||||
<div class="mb-3 flex items-center justify-between gap-3">
|
<div class="mb-3 flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{{ t("admin.groups.modelsList.title") }}
|
{{ t("admin.groups.modelsList.title", { endpoint: modelsListEndpoint(createForm.platform) }) }}
|
||||||
</label>
|
</label>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{{ t("admin.groups.modelsList.hint") }}
|
{{ t("admin.groups.modelsList.hint", { endpoint: modelsListEndpoint(createForm.platform) }) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -2576,10 +2576,10 @@
|
|||||||
<div class="mb-3 flex items-center justify-between gap-3">
|
<div class="mb-3 flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{{ t("admin.groups.modelsList.title") }}
|
{{ t("admin.groups.modelsList.title", { endpoint: modelsListEndpoint(editForm.platform) }) }}
|
||||||
</label>
|
</label>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{{ t("admin.groups.modelsList.hint") }}
|
{{ t("admin.groups.modelsList.hint", { endpoint: modelsListEndpoint(editForm.platform) }) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -5167,6 +5167,8 @@ const createMessagesDispatchDefaults = createDefaultMessagesDispatchFormState();
|
|||||||
const editMessagesDispatchDefaults = createDefaultMessagesDispatchFormState();
|
const editMessagesDispatchDefaults = createDefaultMessagesDispatchFormState();
|
||||||
const createModelsListState = reactive(createInitialModelsListState());
|
const createModelsListState = reactive(createInitialModelsListState());
|
||||||
const editModelsListState = reactive(createInitialModelsListState());
|
const editModelsListState = reactive(createInitialModelsListState());
|
||||||
|
const modelsListEndpoint = (platform: string) =>
|
||||||
|
platform === "gemini" ? "/v1beta/models" : "/v1/models";
|
||||||
const createModelsListLoading = ref(false);
|
const createModelsListLoading = ref(false);
|
||||||
const editModelsListLoading = ref(false);
|
const editModelsListLoading = ref(false);
|
||||||
type ReasoningEffortPolicyFieldsExpose = {
|
type ReasoningEffortPolicyFieldsExpose = {
|
||||||
|
|||||||
@@ -23,4 +23,16 @@ describe("groups models list layout", () => {
|
|||||||
"btn btn-secondary shrink-0 whitespace-nowrap",
|
"btn btn-secondary shrink-0 whitespace-nowrap",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the Gemini-native models endpoint in Gemini group copy", () => {
|
||||||
|
expect(groupsViewSource).toContain(
|
||||||
|
'platform === "gemini" ? "/v1beta/models" : "/v1/models"',
|
||||||
|
);
|
||||||
|
expect(groupsViewSource).toContain(
|
||||||
|
"modelsListEndpoint(createForm.platform)",
|
||||||
|
);
|
||||||
|
expect(groupsViewSource).toContain(
|
||||||
|
"modelsListEndpoint(editForm.platform)",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user