Merge pull request #6095 from xiaxiaxaia/fix/openai-oauth-upstream-model-sync

fix(openai): sync models for OAuth accounts
This commit is contained in:
Wesley Liddick
2026-08-24 11:21:58 +08:00
committed by GitHub
2 changed files with 121 additions and 0 deletions
@@ -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()