mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #4774 from superman2003/fix/issues-4763-4765-4769-20260723
fix: optimize Codex identity imports and OpenAI account tests
This commit is contained in:
@@ -111,7 +111,8 @@ type codexJWTOpenAIClaims struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type codexAccountIndex struct {
|
type codexAccountIndex struct {
|
||||||
accountsByKey map[string][]service.Account
|
accountsByKey map[string][]service.Account
|
||||||
|
keysByAccountID map[int64]map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AccountHandler) ImportCodexSession(c *gin.Context) {
|
func (h *AccountHandler) ImportCodexSession(c *gin.Context) {
|
||||||
@@ -932,7 +933,10 @@ func buildCodexStoredIdentityKeys(accountID, userID, email, accessToken string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildCodexAccountIndex(accounts []service.Account) *codexAccountIndex {
|
func buildCodexAccountIndex(accounts []service.Account) *codexAccountIndex {
|
||||||
index := &codexAccountIndex{accountsByKey: map[string][]service.Account{}}
|
index := &codexAccountIndex{
|
||||||
|
accountsByKey: map[string][]service.Account{},
|
||||||
|
keysByAccountID: map[int64]map[string]struct{}{},
|
||||||
|
}
|
||||||
for _, account := range accounts {
|
for _, account := range accounts {
|
||||||
index.Add(account)
|
index.Add(account)
|
||||||
}
|
}
|
||||||
@@ -946,40 +950,71 @@ func (i *codexAccountIndex) Add(account service.Account) {
|
|||||||
if i.accountsByKey == nil {
|
if i.accountsByKey == nil {
|
||||||
i.accountsByKey = map[string][]service.Account{}
|
i.accountsByKey = map[string][]service.Account{}
|
||||||
}
|
}
|
||||||
i.remove(account.ID)
|
if i.keysByAccountID == nil {
|
||||||
|
i.keysByAccountID = map[int64]map[string]struct{}{}
|
||||||
|
}
|
||||||
keys := buildCodexStoredIdentityKeys(
|
keys := buildCodexStoredIdentityKeys(
|
||||||
codexCredentialString(account.Credentials, "chatgpt_account_id"),
|
codexCredentialString(account.Credentials, "chatgpt_account_id"),
|
||||||
codexCredentialString(account.Credentials, "chatgpt_user_id"),
|
codexCredentialString(account.Credentials, "chatgpt_user_id"),
|
||||||
codexCredentialString(account.Credentials, "email"),
|
codexCredentialString(account.Credentials, "email"),
|
||||||
codexCredentialString(account.Credentials, "access_token"),
|
codexCredentialString(account.Credentials, "access_token"),
|
||||||
)
|
)
|
||||||
|
orderedKeys := make([]string, 0, len(keys)+1)
|
||||||
|
accountKeys := make(map[string]struct{}, len(keys)+1)
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
i.accountsByKey[key] = upsertCodexAccount(i.accountsByKey[key], account)
|
if _, exists := accountKeys[key]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
accountKeys[key] = struct{}{}
|
||||||
|
orderedKeys = append(orderedKeys, key)
|
||||||
}
|
}
|
||||||
if runtimeID := codexCredentialString(account.Credentials, "agent_runtime_id"); runtimeID != "" {
|
if runtimeID := codexCredentialString(account.Credentials, "agent_runtime_id"); runtimeID != "" {
|
||||||
key := "agent:" + runtimeID
|
key := "agent:" + runtimeID
|
||||||
i.accountsByKey[key] = upsertCodexAccount(i.accountsByKey[key], account)
|
if _, exists := accountKeys[key]; !exists {
|
||||||
}
|
accountKeys[key] = struct{}{}
|
||||||
}
|
orderedKeys = append(orderedKeys, key)
|
||||||
|
|
||||||
func (i *codexAccountIndex) remove(accountID int64) {
|
|
||||||
for key, accounts := range i.accountsByKey {
|
|
||||||
kept := accounts[:0]
|
|
||||||
for _, account := range accounts {
|
|
||||||
if account.ID != accountID {
|
|
||||||
kept = append(kept, account)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(kept) == 0 {
|
}
|
||||||
delete(i.accountsByKey, key)
|
|
||||||
|
previousKeys := i.keysByAccountID[account.ID]
|
||||||
|
for key := range previousKeys {
|
||||||
|
if _, retained := accountKeys[key]; retained {
|
||||||
|
i.accountsByKey[key] = upsertCodexAccount(i.accountsByKey[key], account)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
i.accountsByKey[key] = kept
|
i.removeFromKey(key, account.ID)
|
||||||
}
|
}
|
||||||
|
for _, key := range orderedKeys {
|
||||||
|
if _, retained := previousKeys[key]; retained {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i.accountsByKey[key] = append(i.accountsByKey[key], account)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(accountKeys) > 0 {
|
||||||
|
i.keysByAccountID[account.ID] = accountKeys
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(i.keysByAccountID, account.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// upsertCodexAccount 保留同一键下的全部候选账号(共享的 account: 键可对应
|
func (i *codexAccountIndex) removeFromKey(key string, accountID int64) {
|
||||||
// 团队内多个账号),同一账号重复 Add 时原位替换为最新状态。
|
accounts := i.accountsByKey[key]
|
||||||
|
kept := accounts[:0]
|
||||||
|
for _, account := range accounts {
|
||||||
|
if account.ID != accountID {
|
||||||
|
kept = append(kept, account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(kept) == 0 {
|
||||||
|
delete(i.accountsByKey, key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i.accountsByKey[key] = kept
|
||||||
|
}
|
||||||
|
|
||||||
|
// upsertCodexAccount keeps all candidates for shared keys while replacing an
|
||||||
|
// existing account in place so ambiguous legacy matches retain their order.
|
||||||
func upsertCodexAccount(accounts []service.Account, account service.Account) []service.Account {
|
func upsertCodexAccount(accounts []service.Account, account service.Account) []service.Account {
|
||||||
for idx := range accounts {
|
for idx := range accounts {
|
||||||
if accounts[idx].ID == account.ID {
|
if accounts[idx].ID == account.ID {
|
||||||
|
|||||||
@@ -509,6 +509,73 @@ func TestCodexAccountIndexUpsertReplacesSameAccount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCodexAccountIndexUpdateRemovesAllPreviousKeys(t *testing.T) {
|
||||||
|
legacy := service.Account{
|
||||||
|
ID: 50,
|
||||||
|
Credentials: map[string]any{
|
||||||
|
"chatgpt_account_id": "team-old",
|
||||||
|
"chatgpt_user_id": "user-old",
|
||||||
|
"email": "old@example.com",
|
||||||
|
"access_token": "access-old",
|
||||||
|
"agent_runtime_id": "runtime-old",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
index := buildCodexAccountIndex([]service.Account{legacy})
|
||||||
|
|
||||||
|
updated := service.Account{
|
||||||
|
ID: 50,
|
||||||
|
Credentials: map[string]any{
|
||||||
|
"chatgpt_account_id": "team-new",
|
||||||
|
"chatgpt_user_id": "user-new",
|
||||||
|
"email": "new@example.com",
|
||||||
|
"access_token": "access-new",
|
||||||
|
"agent_runtime_id": "runtime-new",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
index.Add(updated)
|
||||||
|
|
||||||
|
oldKeys := append(buildCodexStoredIdentityKeys("team-old", "user-old", "old@example.com", "access-old"), "agent:runtime-old")
|
||||||
|
for _, key := range oldKeys {
|
||||||
|
if got, matchedKey := index.Find([]string{key}, "user-old"); got != nil {
|
||||||
|
t.Fatalf("stale account matched by %q: account ID %d", matchedKey, got.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newKeys := append(buildCodexStoredIdentityKeys("team-new", "user-new", "new@example.com", "access-new"), "agent:runtime-new")
|
||||||
|
for _, key := range newKeys {
|
||||||
|
got, matchedKey := index.Find([]string{key}, "user-new")
|
||||||
|
if got == nil || got.ID != updated.ID {
|
||||||
|
t.Fatalf("updated account not found by %q: account=%v matched=%q", key, got, matchedKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodexAccountIndexUpdatePreservesSharedKeyCandidateOrder(t *testing.T) {
|
||||||
|
first := service.Account{
|
||||||
|
ID: 60,
|
||||||
|
Credentials: map[string]any{
|
||||||
|
"chatgpt_account_id": "team-shared",
|
||||||
|
"access_token": "access-first-old",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
second := service.Account{
|
||||||
|
ID: 61,
|
||||||
|
Credentials: map[string]any{
|
||||||
|
"chatgpt_account_id": "team-shared",
|
||||||
|
"access_token": "access-second",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
index := buildCodexAccountIndex([]service.Account{first, second})
|
||||||
|
|
||||||
|
first.Credentials["access_token"] = "access-first-new"
|
||||||
|
index.Add(first)
|
||||||
|
|
||||||
|
got, matchedKey := index.Find([]string{"account:team-shared"}, "")
|
||||||
|
if got == nil || got.ID != first.ID {
|
||||||
|
t.Fatalf("shared key candidate order changed after update: account=%v matched=%q", got, matchedKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCodexIdentitySeenDistinguishesTeamMembers(t *testing.T) {
|
func TestCodexIdentitySeenDistinguishesTeamMembers(t *testing.T) {
|
||||||
seen := map[string]codexSeenIdentity{}
|
seen := map[string]codexSeenIdentity{}
|
||||||
member1 := buildCodexImportIdentityKeys("team-1", "user-1", "", "token-1", "refresh-1")
|
member1 := buildCodexImportIdentityKeys("team-1", "user-1", "", "token-1", "refresh-1")
|
||||||
|
|||||||
@@ -213,6 +213,38 @@ func TestAccountHandlerGetAvailableModels_OpenAIOAuthPassthroughFallsBackToDefau
|
|||||||
require.NotEqual(t, "gpt-5", resp.Data[0].ID)
|
require.NotEqual(t, "gpt-5", resp.Data[0].ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccountHandlerGetAvailableModels_OpenAIAPIKeyDefaultsToConcreteGPT56Sol(t *testing.T) {
|
||||||
|
svc := &availableModelsAdminService{
|
||||||
|
stubAdminService: newStubAdminService(),
|
||||||
|
account: service.Account{
|
||||||
|
ID: 46,
|
||||||
|
Name: "openai-apikey",
|
||||||
|
Platform: service.PlatformOpenAI,
|
||||||
|
Type: service.AccountTypeAPIKey,
|
||||||
|
Status: service.StatusActive,
|
||||||
|
Credentials: map[string]any{
|
||||||
|
"api_key": "test-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
router := setupAvailableModelsRouter(svc)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/46/models", nil)
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
|
require.NotEmpty(t, resp.Data)
|
||||||
|
require.Equal(t, "gpt-5.6-sol", resp.Data[0].ID)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAccountHandlerGetAvailableModels_OpenAISparkShadowReturnsMappingModels(t *testing.T) {
|
func TestAccountHandlerGetAvailableModels_OpenAISparkShadowReturnsMappingModels(t *testing.T) {
|
||||||
parentID := int64(100)
|
parentID := int64(100)
|
||||||
svc := &availableModelsAdminService{
|
svc := &availableModelsAdminService{
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ type Model struct {
|
|||||||
|
|
||||||
// DefaultModels OpenAI models list
|
// DefaultModels OpenAI models list
|
||||||
var DefaultModels = []Model{
|
var DefaultModels = []Model{
|
||||||
{ID: "gpt-5.6", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 (Sol)"},
|
|
||||||
{ID: "gpt-5.6-sol", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Sol"},
|
{ID: "gpt-5.6-sol", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Sol"},
|
||||||
|
{ID: "gpt-5.6", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 (Sol)"},
|
||||||
{ID: "gpt-5.6-terra", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Terra"},
|
{ID: "gpt-5.6-terra", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Terra"},
|
||||||
{ID: "gpt-5.6-luna", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Luna"},
|
{ID: "gpt-5.6-luna", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Luna"},
|
||||||
{ID: "gpt-5.5", Object: "model", Created: 1776873600, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.5"},
|
{ID: "gpt-5.5", Object: "model", Created: 1776873600, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.5"},
|
||||||
|
|||||||
@@ -9,3 +9,8 @@ import (
|
|||||||
func TestDefaultModelsIncludeBareGPT56Alias(t *testing.T) {
|
func TestDefaultModelsIncludeBareGPT56Alias(t *testing.T) {
|
||||||
require.Contains(t, DefaultModelIDs(), "gpt-5.6")
|
require.Contains(t, DefaultModelIDs(), "gpt-5.6")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultModelsPreferConcreteGPT56SolForAccountTests(t *testing.T) {
|
||||||
|
require.NotEmpty(t, DefaultModels)
|
||||||
|
require.Equal(t, "gpt-5.6-sol", DefaultModels[0].ID)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user