mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
fix(openai): sync models for OAuth accounts
This commit is contained in:
@@ -343,6 +343,9 @@ func (s *AccountTestService) buildAntigravityAPIKeyModelsRequest(ctx context.Con
|
||||
}
|
||||
|
||||
func (s *AccountTestService) buildOpenAIUpstreamModelsRequest(ctx context.Context, account *Account) (*http.Request, error) {
|
||||
if account.IsOpenAIOAuth() {
|
||||
return s.buildOpenAIOAuthUpstreamModelsRequest(ctx, account)
|
||||
}
|
||||
if account.Type != AccountTypeAPIKey {
|
||||
return nil, newUpstreamModelSyncUnsupportedError(
|
||||
fmt.Sprintf("Unsupported OpenAI account type for upstream model sync: %s", account.Type), nil,
|
||||
@@ -375,6 +378,68 @@ func (s *AccountTestService) buildOpenAIUpstreamModelsRequest(ctx context.Contex
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// buildOpenAIOAuthUpstreamModelsRequest uses ChatGPT's Codex model manifest.
|
||||
// OAuth subscriptions do not expose the public Platform API /v1/models endpoint,
|
||||
// so treating them like API-key accounts makes the admin sync button fail locally.
|
||||
func (s *AccountTestService) buildOpenAIOAuthUpstreamModelsRequest(ctx context.Context, account *Account) (*http.Request, error) {
|
||||
credentialAccount, err := resolveCredentialAccount(ctx, s.accountRepo, account)
|
||||
if err != nil {
|
||||
return nil, newUpstreamModelSyncConfigError("Failed to resolve OpenAI account credentials", err)
|
||||
}
|
||||
if !credentialAccount.IsOpenAIOAuth() {
|
||||
return nil, newUpstreamModelSyncUnsupportedError(
|
||||
fmt.Sprintf("Unsupported OpenAI account type for upstream model sync: %s", credentialAccount.Type), nil,
|
||||
)
|
||||
}
|
||||
|
||||
modelsURL, err := buildCodexModelsManifestURL(
|
||||
chatgptCodexModelsURL,
|
||||
false,
|
||||
CodexCanonicalClientVersion(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, newUpstreamModelSyncConfigError("Invalid OpenAI Codex model list URL", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, modelsURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, newUpstreamModelSyncConfigError("Invalid OpenAI Codex model list request", err)
|
||||
}
|
||||
|
||||
if credentialAccount.IsOpenAIAgentIdentity() {
|
||||
authHeaders, authErr := buildAgentIdentityAuthenticationHeaders(
|
||||
ctx,
|
||||
s.accountRepo,
|
||||
s.agentIdentityWS,
|
||||
&s.agentIdentityTaskMu,
|
||||
credentialAccount,
|
||||
)
|
||||
if authErr != nil {
|
||||
return nil, newUpstreamModelSyncUpstreamError("Failed to build OpenAI Agent Identity authentication", authErr)
|
||||
}
|
||||
for key, values := range authHeaders {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
accessToken := strings.TrimSpace(credentialAccount.GetOpenAIAccessToken())
|
||||
if accessToken == "" {
|
||||
return nil, newUpstreamModelSyncConfigError("No OpenAI access token is available", nil)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
}
|
||||
|
||||
identity := resolveCodexOutboundIdentity(credentialAccount.GetOpenAIUserAgent())
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Originator", identity.originator)
|
||||
req.Header.Set("User-Agent", identity.userAgent)
|
||||
req.Header.Set("Version", identity.version)
|
||||
setOpenAIChatGPTAccountHeaders(req.Header, credentialAccount)
|
||||
credentialAccount.ApplyHeaderOverrides(req.Header)
|
||||
enforceCodexIdentityHeadersWithUA(req.Header, credentialAccount.GetOpenAIUserAgent())
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (s *AccountTestService) buildGeminiUpstreamModelsRequest(ctx context.Context, account *Account) (*http.Request, error) {
|
||||
baseURL := account.GetGeminiBaseURL(geminicli.AIStudioBaseURL)
|
||||
if strings.TrimSpace(baseURL) == "" {
|
||||
@@ -498,6 +563,7 @@ func buildGeminiModelsURL(base string) string {
|
||||
|
||||
type upstreamModelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Model string `json:"model"`
|
||||
ModelID string `json:"modelId"`
|
||||
ModelIDSnake string `json:"model_id"`
|
||||
@@ -561,6 +627,9 @@ func extractUpstreamModelIDsWithSelector(body []byte, selectID func(upstreamMode
|
||||
|
||||
func upstreamModelEntryID(entry upstreamModelEntry) string {
|
||||
modelID := strings.TrimSpace(entry.ID)
|
||||
if modelID == "" {
|
||||
modelID = strings.TrimSpace(entry.Slug)
|
||||
}
|
||||
if modelID == "" {
|
||||
modelID = strings.TrimSpace(entry.Name)
|
||||
}
|
||||
|
||||
@@ -140,6 +140,11 @@ func TestExtractUpstreamModelIDs(t *testing.T) {
|
||||
body: `{"data":[{"id":"canonical-id","model":"display-model"}]}`,
|
||||
want: []string{"canonical-id"},
|
||||
},
|
||||
{
|
||||
name: "codex manifest uses slug",
|
||||
body: `{"models":[{"slug":"gpt-5.6-sol"},{"slug":"gpt-5.5-codex"}]}`,
|
||||
want: []string{"gpt-5.5-codex", "gpt-5.6-sol"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -154,6 +159,53 @@ func TestExtractUpstreamModelIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUpstreamModelsRequestSupportsOpenAIOAuth(t *testing.T) {
|
||||
svc := &AccountTestService{cfg: upstreamModelSyncTestConfig()}
|
||||
account := &Account{
|
||||
ID: 11,
|
||||
Platform: PlatformOpenAI,
|
||||
Type: AccountTypeOAuth,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "openai-oauth-token",
|
||||
"chatgpt_account_id": "chatgpt-account",
|
||||
},
|
||||
}
|
||||
|
||||
req, err := svc.buildUpstreamModelsRequest(context.Background(), account)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, chatgptCodexModelsURL, req.URL.Scheme+"://"+req.URL.Host+req.URL.Path)
|
||||
require.NotEmpty(t, req.URL.Query().Get("client_version"))
|
||||
require.Equal(t, "Bearer openai-oauth-token", req.Header.Get("Authorization"))
|
||||
require.Equal(t, "chatgpt-account", req.Header.Get("chatgpt-account-id"))
|
||||
require.NotEmpty(t, req.Header.Get("Originator"))
|
||||
require.NotEmpty(t, req.Header.Get("User-Agent"))
|
||||
require.NotEmpty(t, req.Header.Get("Version"))
|
||||
}
|
||||
|
||||
func TestFetchUpstreamSupportedModelsParsesOpenAIOAuthManifest(t *testing.T) {
|
||||
upstream := &httpUpstreamRecorder{resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{"models":[{"slug":"gpt-5.6-sol"},{"slug":"gpt-5.5-codex"}]}`)),
|
||||
}}
|
||||
svc := &AccountTestService{
|
||||
httpUpstream: upstream,
|
||||
cfg: upstreamModelSyncTestConfig(),
|
||||
}
|
||||
|
||||
models, err := svc.FetchUpstreamSupportedModels(context.Background(), &Account{
|
||||
ID: 12,
|
||||
Platform: PlatformOpenAI,
|
||||
Type: AccountTypeOAuth,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "openai-oauth-token",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"gpt-5.5-codex", "gpt-5.6-sol"}, models)
|
||||
require.Equal(t, "Bearer openai-oauth-token", upstream.lastReq.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func TestExtractGrokUpstreamModelIDs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user