fix(openai): keep OAuth image aliases in the test picker

Image choices were resolved from DefaultModels IDs and from mapping keys, so an
alias whose target is a local image model (paint -> gpt-image-2.5-flare) never
reached the picker: IsModelSupported only matches keys, and a public name is not
an image model. Resolve the public name to its target before deciding, and only
synthesize an entry when the target is an image model.

Update the fixture of
TestFetchOpenAIAccountModelsOAuthLabelsLocalImageModelsLikeUpstream: it asserted
on an upstream slug the account did not configure, which conflicts with the
account-scoped projection. Coverage is preserved by configuring that slug and by
a new test asserting unconfigured text models stay out.
This commit is contained in:
Thinker-Joe
2026-09-22 21:15:20 +08:00
parent de28eea11f
commit 4daea093f9
2 changed files with 70 additions and 4 deletions
@@ -5,6 +5,7 @@ import (
"net/http"
"testing"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/stretchr/testify/require"
)
@@ -117,7 +118,13 @@ func TestFetchOpenAIAccountModelsOAuthLabelsLocalImageModelsLikeUpstream(t *test
newCodexModelsOAuthCacheServer(t, `{"models":[{"slug":"gpt-5.6-sol"}]}`)
svc := &AccountTestService{openaiGatewayService: &OpenAIGatewayService{}}
account := newCodexModelsTestAccount()
account.Credentials["model_mapping"] = map[string]any{"gpt-image-2.5-flare": "gpt-image-2.5-flare"}
// This case is about label sources, so it must configure the upstream slug it
// asserts on: the picker is scoped to the account mapping, and an unconfigured
// slug is intentionally absent (see the exclusion test below).
account.Credentials["model_mapping"] = map[string]any{
"gpt-5.6-sol": "gpt-5.6-sol",
"gpt-image-2.5-flare": "gpt-image-2.5-flare",
}
models, err := svc.FetchOpenAIAccountModels(context.Background(), account)
require.NoError(t, err)
byID := make(map[string]string, len(models))
@@ -142,3 +149,51 @@ func TestFetchOpenAIAccountModelsOAuthRespectsImageAllowlist(t *testing.T) {
require.Contains(t, ids, "gpt-image-2.5-flare")
require.NotContains(t, ids, "gpt-image-2.5-sunburst")
}
func pickerModelIDs(models []openai.Model) []string {
ids := make([]string, 0, len(models))
for _, model := range models {
ids = append(ids, model.ID)
}
return ids
}
// The picker is scoped to the account mapping: an upstream slug the account does
// not configure must not become a testable choice.
func TestFetchOpenAIAccountModelsOAuthExcludesUnconfiguredTextModels(t *testing.T) {
newCodexModelsOAuthCacheServer(t, `{"models":[{"slug":"gpt-5.6-sol"},{"slug":"gpt-6-astra"}]}`)
svc := &AccountTestService{openaiGatewayService: &OpenAIGatewayService{}}
account := newCodexModelsTestAccount()
account.Credentials["model_mapping"] = map[string]any{"gpt-5.6-sol": "gpt-5.6-sol"}
models, err := svc.FetchOpenAIAccountModels(context.Background(), account)
require.NoError(t, err)
require.Equal(t, []string{"gpt-5.6-sol"}, pickerModelIDs(models))
}
// An alias may point at a local image model that Codex discovery never lists, so
// the picker has to resolve the target before deciding to offer the public name.
func TestFetchOpenAIAccountModelsOAuthLocalImageAlias(t *testing.T) {
newCodexModelsOAuthCacheServer(t, `{"models":[{"slug":"gpt-6-astra"}]}`)
svc := &AccountTestService{openaiGatewayService: &OpenAIGatewayService{}}
account := newCodexModelsTestAccount()
account.Credentials["model_mapping"] = map[string]any{"paint": "gpt-image-2.5-flare"}
models, err := svc.FetchOpenAIAccountModels(context.Background(), account)
require.NoError(t, err)
require.Equal(t, []string{"paint"}, pickerModelIDs(models))
require.Equal(t, "paint", models[0].DisplayName)
}
// A public name that merely looks like an image model must not be enough to
// synthesize a picker entry when its target is a text model.
func TestFetchOpenAIAccountModelsOAuthImageLookalikeAliasIsNotSynthesized(t *testing.T) {
newCodexModelsOAuthCacheServer(t, `{"models":[{"slug":"gpt-5.6-sol"}]}`)
svc := &AccountTestService{openaiGatewayService: &OpenAIGatewayService{}}
account := newCodexModelsTestAccount()
account.Credentials["model_mapping"] = map[string]any{"gpt-image-2.5-lookalike": "text-target-missing"}
models, err := svc.FetchOpenAIAccountModels(context.Background(), account)
require.NoError(t, err)
require.Empty(t, pickerModelIDs(models))
}
@@ -228,10 +228,21 @@ func (s *AccountTestService) FetchOpenAIAccountModels(ctx context.Context, accou
seen[model.ID] = true
}
}
for model := range account.GetModelMapping() {
if IsGPTImageGenerationModel(model) && !strings.Contains(model, "*") && !seen[model] {
payload.Data = append(payload.Data, openai.Model{ID: model, Object: "model", Type: "model", OwnedBy: "openai", DisplayName: openaiCodexDisplayName(model)})
// Image models that a configured alias points at are absent from the Codex
// manifest, so the projection alone cannot surface them. Resolve each public
// name to its target and keep the entry when that target is an image model.
// Judging by the target rather than the public name keeps a lookalike name
// (for example an alias spelled "gpt-image-*" that maps to a text model)
// from being synthesized into the picker.
for publicID := range account.GetModelMapping() {
if strings.Contains(publicID, "*") || seen[publicID] {
continue
}
if !IsGPTImageGenerationModel(account.GetMappedModel(publicID)) {
continue
}
payload.Data = append(payload.Data, openai.Model{ID: publicID, Object: "model", Type: "model", OwnedBy: "openai", DisplayName: openaiCodexDisplayName(publicID)})
seen[publicID] = true
}
}
return payload.Data, nil