Skip to content

Commit 59066cf

Browse files
committed
release: 发布 1.2.76 并更新模型目录
1 parent b2a5042 commit 59066cf

36 files changed

Lines changed: 396 additions & 61 deletions

backend/cmd/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.2.75
1+
1.2.76

backend/internal/handler/admin/account_handler_available_models_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func TestAccountHandlerGetAvailableModels_OwnedAccountReturnsPricedIntersection(
6868
Type: service.AccountTypeOAuth,
6969
Status: service.StatusActive,
7070
OwnerUserID: &ownerUserID,
71+
ShareMode: service.AccountShareModePublic,
7172
Credentials: map[string]any{
7273
"model_mapping": map[string]any{
7374
"gpt-5": "gpt-5.1",
@@ -164,6 +165,7 @@ func TestAccountHandlerGetAvailableModels_OwnedAccountEmptyWhitelistReturnsWhite
164165
Type: service.AccountTypeOAuth,
165166
Status: service.StatusActive,
166167
OwnerUserID: &ownerUserID,
168+
ShareMode: service.AccountShareModePublic,
167169
Credentials: map[string]any{},
168170
},
169171
}
@@ -188,6 +190,7 @@ func TestAccountHandlerGetAvailableModels_OwnedAccountNoPricedIntersection(t *te
188190
Type: service.AccountTypeOAuth,
189191
Status: service.StatusActive,
190192
OwnerUserID: &ownerUserID,
193+
ShareMode: service.AccountShareModePublic,
191194
Credentials: map[string]any{
192195
"model_mapping": map[string]any{"gpt-9": "gpt-9"},
193196
},

backend/internal/handler/admin/account_handler_batch_test_model_options_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func ownedGrokAccountForBatchTest(id int64, mapping map[string]any) *service.Acc
4545
Type: service.AccountTypeOAuth,
4646
Status: service.StatusActive,
4747
OwnerUserID: &ownerID,
48+
ShareMode: service.AccountShareModePublic,
4849
Credentials: credentials,
4950
}
5051
}
@@ -107,6 +108,7 @@ func TestAccountHandlerGetBatchTestModelOptions_MixedPlatforms(t *testing.T) {
107108
Type: service.AccountTypeOAuth,
108109
Status: service.StatusActive,
109110
OwnerUserID: &ownerID,
111+
ShareMode: service.AccountShareModePublic,
110112
Credentials: map[string]any{"model_mapping": map[string]any{"gpt-5": "gpt-5"}},
111113
},
112114
},

backend/internal/handler/user_account_handler.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2386,7 +2386,12 @@ func (h *UserAccountHandler) Test(c *gin.Context) {
23862386
response.BadRequest(c, "model_id is required")
23872387
return
23882388
}
2389-
if !account.IsModelSupported(req.ModelID) {
2389+
if account.UsesPlatformModelInheritance() {
2390+
if err := h.accountTestService.ValidateExplicitTestModel(c.Request.Context(), account, req.ModelID); err != nil {
2391+
response.ErrorFrom(c, err)
2392+
return
2393+
}
2394+
} else if !account.IsModelSupported(req.ModelID) {
23902395
response.ErrorFrom(c, service.ErrOwnedAccountModelNotSelectable.WithMetadata(map[string]string{
23912396
"platform": account.Platform,
23922397
"model": req.ModelID,
@@ -2407,7 +2412,7 @@ func (h *UserAccountHandler) Test(c *gin.Context) {
24072412

24082413
// GetAvailableModels handles getting available models for a user-owned account.
24092414
// GET /api/v1/accounts/:id/models
2410-
// 复用 service.AvailableTestModels,用户端严格遵守个人账号模型白名单
2415+
// 私有账号继承平台定价目录,公有账号按模型白名单与定价目录取交集
24112416
func (h *UserAccountHandler) GetAvailableModels(c *gin.Context) {
24122417
subject, ok := middleware2.GetAuthSubjectFromContext(c)
24132418
if !ok {

backend/internal/handler/user_account_public_share_test.go

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"strings"
1616
"testing"
1717

18+
"github.com/Wei-Shaw/sub2api/internal/config"
1819
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
1920
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
2021
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
@@ -29,6 +30,20 @@ const (
2930
userAgentIdentityPublicGroupID int64 = 8202
3031
)
3132

33+
type userAccountTestModelCatalog struct{}
34+
35+
func (userAccountTestModelCatalog) ListPricedModelIDs(context.Context, []string) ([]string, error) {
36+
return nil, nil
37+
}
38+
39+
func (userAccountTestModelCatalog) ListSelectablePricedModelIDs(context.Context, service.PricedModelQuery) ([]string, error) {
40+
return []string{"selected-model", "gpt-new"}, nil
41+
}
42+
43+
func (userAccountTestModelCatalog) IsModelPriced(context.Context, service.PricedModelQuery, string) (bool, error) {
44+
return false, nil
45+
}
46+
3247
type userAgentIdentityShareRepo struct {
3348
service.AccountRepository
3449
accounts map[int64]*service.Account
@@ -259,6 +274,7 @@ type userAgentIdentityValidationUpstream struct {
259274
body string
260275
calls int
261276
lastAuthorization string
277+
lastModel string
262278
}
263279

264280
func (u *userAgentIdentityValidationUpstream) Do(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
@@ -272,6 +288,13 @@ func (u *userAgentIdentityValidationUpstream) DoWithTLS(req *http.Request, _ str
272288
func (u *userAgentIdentityValidationUpstream) response(req *http.Request) *http.Response {
273289
u.calls++
274290
u.lastAuthorization = req.Header.Get("Authorization")
291+
if req.Body != nil {
292+
var payload struct {
293+
Model string `json:"model"`
294+
}
295+
_ = json.NewDecoder(req.Body).Decode(&payload)
296+
u.lastModel = payload.Model
297+
}
275298
statusCode := u.statusCode
276299
if statusCode == 0 {
277300
statusCode = http.StatusOK
@@ -368,7 +391,8 @@ func newUserAgentIdentityShareHandler(
368391
accountService.SetAccountShareModeRepository(placementRepo)
369392
accountService.SetAgentIdentityWSInvalidator(invalidatorProxy)
370393
upstream := &userAgentIdentityValidationUpstream{statusCode: upstreamStatus, body: upstreamBody}
371-
accountTestService := service.NewAccountTestService(repo, nil, nil, nil, upstream, nil, nil, nil, invalidatorProxy)
394+
accountTestService := service.NewAccountTestService(repo, nil, nil, nil, upstream, &config.Config{}, nil, nil, invalidatorProxy)
395+
accountTestService.SetModelResolver(service.NewAccountTestModelResolver(userAccountTestModelCatalog{}))
372396
handler := NewUserAccountHandler(accountService, nil, accountTestService, nil, nil, nil, nil, nil, nil, nil)
373397
return handler, repo, upstream, invalidator, placementRepo
374398
}
@@ -389,10 +413,10 @@ func runUserAgentIdentityUpdateRequest(t *testing.T, handler *UserAccountHandler
389413
return recorder
390414
}
391415

392-
func TestUserAccountHandlerTestRejectsModelOutsideOwnerWhitelist(t *testing.T) {
416+
func TestUserAccountHandlerTestRejectsModelOutsideOwnerWhitelistForPublicAccount(t *testing.T) {
393417
gin.SetMode(gin.TestMode)
394418
ownerUserID := int64(101)
395-
account := newUserAgentIdentityShareAccount(t, ownerUserID, service.AccountShareModePrivate, service.AccountShareStatusApproved)
419+
account := newUserAgentIdentityShareAccount(t, ownerUserID, service.AccountShareModePublic, service.AccountShareStatusApproved)
396420
account.Credentials["model_mapping"] = map[string]any{"selected-model": "selected-model"}
397421
handler, _, upstream, _, _ := newUserAgentIdentityShareHandler(t, account, http.StatusOK, "")
398422

@@ -412,6 +436,53 @@ func TestUserAccountHandlerTestRejectsModelOutsideOwnerWhitelist(t *testing.T) {
412436
require.Zero(t, upstream.calls)
413437
}
414438

439+
func TestUserAccountHandlerTestPrivateInheritanceAllowsPricedModelOutsideLegacyMapping(t *testing.T) {
440+
gin.SetMode(gin.TestMode)
441+
ownerUserID := int64(101)
442+
account := newUserAgentIdentityShareAccount(t, ownerUserID, service.AccountShareModePrivate, service.AccountShareStatusApproved)
443+
account.Type = service.AccountTypeAPIKey
444+
account.Credentials = map[string]any{"api_key": "test-key", "model_mapping": map[string]any{"old-model": "old-model"}}
445+
handler, _, upstream, _, _ := newUserAgentIdentityShareHandler(t, account, http.StatusOK, "")
446+
447+
router := gin.New()
448+
router.POST("/accounts/:id/test", func(c *gin.Context) {
449+
c.Set(string(middleware2.ContextKeyUser), middleware2.AuthSubject{UserID: ownerUserID})
450+
handler.Test(c)
451+
})
452+
recorder := httptest.NewRecorder()
453+
request := httptest.NewRequest(http.MethodPost, "/accounts/1/test", strings.NewReader(`{"model_id":"gpt-new"}`))
454+
request.Header.Set("Content-Type", "application/json")
455+
router.ServeHTTP(recorder, request)
456+
457+
require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())
458+
require.Contains(t, recorder.Body.String(), `"success":true`)
459+
require.Equal(t, 1, upstream.calls)
460+
require.Equal(t, "gpt-new", upstream.lastModel)
461+
}
462+
463+
func TestUserAccountHandlerTestPrivateInheritanceRejectsUnpricedModel(t *testing.T) {
464+
gin.SetMode(gin.TestMode)
465+
ownerUserID := int64(101)
466+
account := newUserAgentIdentityShareAccount(t, ownerUserID, service.AccountShareModePrivate, service.AccountShareStatusApproved)
467+
account.Type = service.AccountTypeAPIKey
468+
account.Credentials = map[string]any{"api_key": "test-key"}
469+
handler, _, upstream, _, _ := newUserAgentIdentityShareHandler(t, account, http.StatusOK, "")
470+
471+
router := gin.New()
472+
router.POST("/accounts/:id/test", func(c *gin.Context) {
473+
c.Set(string(middleware2.ContextKeyUser), middleware2.AuthSubject{UserID: ownerUserID})
474+
handler.Test(c)
475+
})
476+
recorder := httptest.NewRecorder()
477+
request := httptest.NewRequest(http.MethodPost, "/accounts/1/test", strings.NewReader(`{"model_id":"unpriced-model"}`))
478+
request.Header.Set("Content-Type", "application/json")
479+
router.ServeHTTP(recorder, request)
480+
481+
require.Equal(t, http.StatusBadRequest, recorder.Code, recorder.Body.String())
482+
require.Contains(t, recorder.Body.String(), "ACCOUNT_TEST_MODEL_NOT_AVAILABLE")
483+
require.Zero(t, upstream.calls)
484+
}
485+
415486
func TestIsOpenAIUsageLimitReachedValidationError(t *testing.T) {
416487
require.True(t, isOpenAIUsageLimitReachedValidationError(`API returned 429: {"error":{"type":"usage_limit_reached","message":"The usage limit has been reached"}}`))
417488
require.True(t, isOpenAIUsageLimitReachedValidationError(`API returned 429: {"error": {"type": "usage_limit_reached"}}`))

backend/internal/repository/account_share_mode_repo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2825,7 +2825,7 @@ func validateAccountShareRoomAllowedModelsInTx(
28252825
Extra: candidate.Extra,
28262826
}
28272827
for _, model := range allowedModels {
2828-
if account.IsModelSupported(model) {
2828+
if account.IsModelSupportedByMapping(model) {
28292829
continue
28302830
}
28312831
return service.ErrAccountShareModeUnsupportedModel.WithMetadata(map[string]string{

backend/internal/repository/account_share_room_repo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ func (r *accountShareModeRepository) AttachRoomAccountsAtomic(
654654
Extra: candidate.Extra,
655655
}
656656
for _, model := range room.AllowedModels {
657-
if account.IsModelSupported(model) {
657+
if account.IsModelSupportedByMapping(model) {
658658
continue
659659
}
660660
recordFailure(accountID, service.ErrAccountShareModeUnsupportedModel, map[string]string{"model": model})

backend/internal/service/account.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,16 @@ func NormalizeAccountShareStatus(status string) string {
659659
}
660660
}
661661

662+
// UsesPlatformModelInheritance reports whether an owned account inherits the
663+
// active model catalog of its platform instead of using its group bindings as
664+
// the model-list scope. Explicit mappings are still honored as upstream
665+
// aliases when a request matches one.
666+
func (a *Account) UsesPlatformModelInheritance() bool {
667+
return a != nil &&
668+
a.OwnerUserID != nil &&
669+
NormalizeAccountShareMode(a.ShareMode) == AccountShareModePrivate
670+
}
671+
662672
func (a *Account) IsPublicShareApproved() bool {
663673
return a != nil &&
664674
a.OwnerUserID != nil &&
@@ -1336,9 +1346,18 @@ func resolveRequestedModelInMapping(mapping map[string]string, requestedModel st
13361346
return matchWildcardMappingResult(mapping, requestedModel)
13371347
}
13381348

1339-
// IsModelSupported 检查模型是否在 model_mapping 中(支持通配符)。
1340-
// 平台账号未配置 mapping 时保持历史兼容(允许所有);个人账号的空白名单拒绝全部
1349+
// IsModelSupported 检查账号模型能力。私有个人账号不以 mapping 限制能力,
1350+
// 可选模型由调用方的渠道定价目录确定;其他账号按 mapping 判断
13411351
func (a *Account) IsModelSupported(requestedModel string) bool {
1352+
if a.UsesPlatformModelInheritance() {
1353+
return strings.TrimSpace(requestedModel) != ""
1354+
}
1355+
return a.IsModelSupportedByMapping(requestedModel)
1356+
}
1357+
1358+
// IsModelSupportedByMapping 保留共享场景的账号模型能力边界(支持通配符)。
1359+
// 个人账号空 mapping 拒绝全部,平台账号未配置 mapping 时保持历史兼容。
1360+
func (a *Account) IsModelSupportedByMapping(requestedModel string) bool {
13421361
mapping := a.GetModelMapping()
13431362
if len(mapping) == 0 {
13441363
return a.OwnerUserID == nil

backend/internal/service/account_opencode_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ func TestResolveOpencodeGoModelSpec(t *testing.T) {
7878
found bool
7979
}{
8080
{name: "chat", model: "deepseek-v4-flash", protocol: OpencodeGoProtocolChat, found: true},
81+
{name: "deepseek flash alias", model: "deepseek-flash", protocol: OpencodeGoProtocolChat, found: true},
82+
{name: "deepseek v4.1 flash", model: "deepseek-v4.1-flash", protocol: OpencodeGoProtocolChat, found: true},
8183
{name: "messages", model: "opencode-go/minimax-m3[1m]", protocol: OpencodeGoProtocolMessages, found: true},
8284
{name: "documented qwen messages", model: "opencode-go/qwen3.7-plus", protocol: OpencodeGoProtocolMessages, found: true},
8385
{name: "responses", model: "opencode/grok-4.6", protocol: OpencodeGoProtocolResponses, found: true},
@@ -154,11 +156,11 @@ func TestOpencodeDefaultModelSlugs(t *testing.T) {
154156
"minimax-m3", "minimax-m2.7", "minimax-m2.5",
155157
"kimi-k3", "kimi-k2.7-code", "kimi-k2.6", "longcat-2.0", "kimi-k2.5",
156158
"glm-5.2", "glm-5.3-flash", "glm-5.3", "glm-5.1", "glm-5",
157-
"deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp",
159+
"deepseek-v4-pro", "deepseek-v4-flash", "deepseek-flash", "deepseek-v4.1-flash", "deepseek-v4-flash-vision-exp",
158160
"qwen3.7-max", "qwen3.8-max", "qwen3.8-flash", "qwen3.7-plus", "qwen3.6-plus", "qwen3.5-plus",
159161
"mimo-v2-pro", "mimo-v2-omni", "mimo-v2.5-pro", "mimo-v2.5",
160162
"hy4-preview", "hy3", "hy3-preview",
161-
"gpt-5.6-luna", "grok-4.5", "grok-4.6", "muse-spark-1.2-contributor", "muse-spark-1.3-contributor", "omen-alpha",
163+
"gpt-5.6-luna", "grok-4.5", "grok-4.6", "muse-spark-1.3-contributor", "muse-spark-1.2-contributor", "omen-alpha",
162164
}
163165
if !slices.Equal(models, wantModels) {
164166
t.Fatalf("model snapshot mismatch\n got: %v\nwant: %v", models, wantModels)
@@ -225,7 +227,7 @@ func TestOpencodeDefaultModelSlugs(t *testing.T) {
225227
}
226228

227229
models[0] = "mutated"
228-
if fresh := OpencodeDefaultModelSlugs(); len(fresh) != 35 || fresh[0] == "mutated" {
230+
if fresh := OpencodeDefaultModelSlugs(); len(fresh) != 37 || fresh[0] == "mutated" {
229231
t.Fatalf("OpencodeDefaultModelSlugs did not return an independent copy: %v", fresh)
230232
}
231233
}

backend/internal/service/account_share_lifecycle.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,7 +1147,7 @@ func (s *AccountShareModeService) validateRoomActivation(
11471147
return ErrAccountShareAccountUnavailable
11481148
}
11491149
for _, model := range allowedModels {
1150-
if account.IsModelSupported(model) {
1150+
if account.IsModelSupportedByMapping(model) {
11511151
continue
11521152
}
11531153
return ErrAccountShareModeUnsupportedModel.WithMetadata(map[string]string{
@@ -1167,7 +1167,7 @@ func (s *AccountShareModeService) validateRoomActivation(
11671167
// OpenCode 房间的恢复校验只需要确认账号凭证和上游连通性,不应使用房间
11681168
// 白名单中的任意模型作为探针。白名单首项可能是区域、套餐或上游状态
11691169
// 不稳定的模型(例如 grok-4.5),会把模型级失败误判成账号不可用。
1170-
// OpenCode 账号测试服务的默认探针是 deepseek-v4-flash,这里显式固定
1170+
// OpenCode 账号测试服务的默认探针是 deepseek-v4.1-flash,这里显式固定
11711171
// 使用同一个模型,避免房间恢复流程传入首个白名单模型覆盖该默认值。
11721172
validationCtx, validationCancel := context.WithTimeout(
11731173
ctx,

0 commit comments

Comments
 (0)