feat(codex): complete routed model catalogs

Codex treats the downloaded model catalog as a capability contract. Slug-only entries hide supported reasoning levels, can advertise invalid image inputs, and expose automatic or media-only models in the model picker. Generate complete route-aware metadata, preserve official manifests, filter non-agent models, and compute validators from the final catalog.
This commit is contained in:
Long Li
2026-08-22 14:43:40 +09:00
parent 22e1b8144a
commit e471be7302
12 changed files with 2420 additions and 97 deletions
+24 -4
View File
@@ -1150,22 +1150,42 @@ func (h *GatewayHandler) CodexModels(c *gin.Context) {
return
}
modelIDs := h.codexModelIDsForGroup(c.Request.Context(), apiKey.Group)
body, err := service.BuildCodexModelsManifest(modelIDs)
forcedPlatform := ""
if value, exists := middleware2.GetForcePlatformFromContext(c); exists {
forcedPlatform = strings.TrimSpace(value)
}
modelIDs := h.codexModelIDsForGroup(c.Request.Context(), apiKey.Group, forcedPlatform)
modelIDs = service.FilterCodexModelIDsForGroup(modelIDs, apiKey.Group)
body, err := h.gatewayService.BuildCodexModelsManifestForGroup(
c.Request.Context(),
apiKey.Group,
forcedPlatform,
modelIDs,
)
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to build Codex models manifest")
return
}
etag := service.CodexModelsManifestETag(body)
c.Header("ETag", etag)
if service.CodexModelsManifestETagMatches(c.GetHeader("If-None-Match"), etag) {
c.Status(http.StatusNotModified)
c.Writer.WriteHeaderNow()
return
}
c.Data(http.StatusOK, "application/json", body)
}
func (h *GatewayHandler) codexModelIDsForGroup(ctx context.Context, group *service.Group) []string {
func (h *GatewayHandler) codexModelIDsForGroup(ctx context.Context, group *service.Group, platformOverride string) []string {
if h == nil || h.gatewayService == nil || group == nil {
return nil
}
groupID := &group.ID
platform := group.Platform
platform := strings.TrimSpace(platformOverride)
if platform == "" {
platform = group.Platform
}
if platform == service.PlatformComposite {
availableModels := h.compositeAvailableModels(ctx, groupID)
fallbackModels := defaultModelIDsForPlatform(service.PlatformComposite)
@@ -27,10 +27,20 @@ type gatewayModelsResponseForTest struct {
type codexModelsResponseForTest struct {
Models []struct {
Slug string `json:"slug"`
Slug string `json:"slug"`
SupportedReasoningLevels []codexReasoningLevelForTest `json:"supported_reasoning_levels"`
InputModalities []string `json:"input_modalities"`
ModelMessages map[string]json.RawMessage `json:"model_messages"`
TruncationPolicy map[string]json.RawMessage `json:"truncation_policy"`
AvailabilityNUX json.RawMessage `json:"availability_nux"`
Upgrade json.RawMessage `json:"upgrade"`
} `json:"models"`
}
type codexReasoningLevelForTest struct {
Effort string `json:"effort"`
}
type gatewayModelItemForTest struct {
ID string `json:"id"`
Object string `json:"object"`
@@ -79,12 +89,33 @@ func TestDefaultModelIDsForCompositeIncludesAntigravityDefaults(t *testing.T) {
// Scenario: non-OpenAI groups return a Codex manifest instead of a standard model list.
func TestGatewayCodexModels_NonOpenAIGroupsUseMappedModels(t *testing.T) {
tests := []struct {
name string
platform string
model string
name string
platform string
model string
efforts []string
modalities []string
}{
{name: "Grok", platform: service.PlatformGrok, model: "grok-4.6"},
{name: "DeepSeek", platform: service.PlatformDeepseek, model: "deepseek-v4-pro"},
{
name: "Grok",
platform: service.PlatformGrok,
model: "grok-4.6",
efforts: []string{"low", "medium", "high", "xhigh"},
modalities: []string{"text", "image"},
},
{
name: "DeepSeek",
platform: service.PlatformDeepseek,
model: "deepseek-v4-pro",
efforts: []string{"low", "high", "max"},
modalities: []string{"text"},
},
{
name: "provider-qualified Claude",
platform: service.PlatformAnthropic,
model: "anthropic/claude-sonnet-4-6",
efforts: []string{"low", "medium", "high", "max"},
modalities: []string{"text"},
},
}
for index, tt := range tests {
@@ -119,6 +150,12 @@ func TestGatewayCodexModels_NonOpenAIGroupsUseMappedModels(t *testing.T) {
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
require.Len(t, got.Models, 1)
require.Equal(t, tt.model, got.Models[0].Slug)
require.NotEmpty(t, got.Models[0].ModelMessages)
require.NotEmpty(t, got.Models[0].TruncationPolicy)
require.NotNil(t, got.Models[0].AvailabilityNUX)
require.NotNil(t, got.Models[0].Upgrade)
require.Equal(t, tt.efforts, codexReasoningEffortsForTest(got.Models[0].SupportedReasoningLevels))
require.Equal(t, tt.modalities, got.Models[0].InputModalities)
})
}
}
@@ -163,6 +200,45 @@ func TestGatewayCodexModels_CompositeUsesCompleteEffectiveModelList(t *testing.T
require.Equal(t, []string{"gpt-5.5", "grok-4.6"}, codexModelSlugsForTest(got.Models))
}
func TestGatewayCodexModels_GeneratedManifestUsesFinalBodyETag(t *testing.T) {
gin.SetMode(gin.TestMode)
const groupID int64 = 122
h := newGatewayModelsHandlerForTest(&gatewayModelsAccountRepoStub{
byGroup: map[int64][]service.Account{
groupID: {{
ID: 1,
Platform: service.PlatformDeepseek,
Credentials: map[string]any{
"model_mapping": map[string]any{"deepseek-v4-pro": "deepseek-v4-pro"},
},
}},
},
})
group := &service.Group{ID: groupID, Platform: service.PlatformDeepseek}
first := httptest.NewRecorder()
firstContext, _ := gin.CreateTestContext(first)
firstContext.Request = httptest.NewRequest(http.MethodGet, "/models?client_version=0.147.0", nil)
firstContext.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{Group: group})
h.CodexModels(firstContext)
require.Equal(t, http.StatusOK, first.Code)
etag := first.Header().Get("ETag")
require.NotEmpty(t, etag)
require.Equal(t, service.CodexModelsManifestETag(first.Body.Bytes()), etag)
second := httptest.NewRecorder()
secondContext, _ := gin.CreateTestContext(second)
secondContext.Request = httptest.NewRequest(http.MethodGet, "/models?client_version=0.147.0", nil)
secondContext.Request.Header.Set("If-None-Match", "W/"+etag)
secondContext.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{Group: group})
h.CodexModels(secondContext)
require.Equal(t, http.StatusNotModified, second.Code)
require.Empty(t, second.Body.Bytes())
require.Equal(t, etag, second.Header().Get("ETag"))
}
// Scenario: group models_list_config limits the generated Codex manifest.
func TestGatewayCodexModels_CustomModelsListFiltersCompositeManifest(t *testing.T) {
gin.SetMode(gin.TestMode)
@@ -211,7 +287,13 @@ func TestGatewayCodexModels_CustomModelsListFiltersCompositeManifest(t *testing.
}
func codexModelSlugsForTest(models []struct {
Slug string `json:"slug"`
Slug string `json:"slug"`
SupportedReasoningLevels []codexReasoningLevelForTest `json:"supported_reasoning_levels"`
InputModalities []string `json:"input_modalities"`
ModelMessages map[string]json.RawMessage `json:"model_messages"`
TruncationPolicy map[string]json.RawMessage `json:"truncation_policy"`
AvailabilityNUX json.RawMessage `json:"availability_nux"`
Upgrade json.RawMessage `json:"upgrade"`
}) []string {
slugs := make([]string, 0, len(models))
for _, model := range models {
@@ -220,6 +302,14 @@ func codexModelSlugsForTest(models []struct {
return slugs
}
func codexReasoningEffortsForTest(levels []codexReasoningLevelForTest) []string {
efforts := make([]string, 0, len(levels))
for _, level := range levels {
efforts = append(efforts, level.Effort)
}
return efforts
}
func TestGatewayModels_GeminiGroupFallsBackToGeminiModels(t *testing.T) {
gin.SetMode(gin.TestMode)
@@ -56,7 +56,10 @@ func (h *OpenAIGatewayHandler) CodexModels(c *gin.Context) {
// 让 ops 错误日志携带实际选中的上游账号,便于定位失效账号(#4544)。
setOpsSelectedAccount(c, account.ID, account.Platform)
manifest, err := h.gatewayService.FetchCodexModelsManifest(c.Request.Context(), account, c.Query("client_version"), c.GetHeader("If-None-Match"))
ifNoneMatch := c.GetHeader("If-None-Match")
// The client ETag represents the final group-specific body, so fetch the
// source manifest before applying local filtering and alias metadata.
manifest, err := h.gatewayService.FetchCodexModelsManifest(c.Request.Context(), account, c.Query("client_version"), "")
if err != nil {
if c.Request.Context().Err() != nil {
return
@@ -70,6 +73,14 @@ func (h *OpenAIGatewayHandler) CodexModels(c *gin.Context) {
h.errorResponse(c, infraerrors.Code(err), "upstream_error", infraerrors.Message(err))
return
}
if err := h.gatewayService.CompleteAPIKeyCodexModelsManifestForClient(manifest, account); err != nil {
h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to complete Codex models manifest")
return
}
if err := h.gatewayService.MergeGroupConfiguredCodexModels(c.Request.Context(), apiKey.Group, manifest, ifNoneMatch); err != nil {
h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to build Codex models manifest")
return
}
if c.Request.Context().Err() != nil {
return
}
@@ -79,6 +90,7 @@ func (h *OpenAIGatewayHandler) CodexModels(c *gin.Context) {
}
if manifest.NotModified {
c.Status(http.StatusNotModified)
c.Writer.WriteHeaderNow()
return
}
c.Data(http.StatusOK, "application/json", manifest.Body)
@@ -2,6 +2,7 @@ package handler
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
@@ -43,6 +44,10 @@ func (r codexModelsFailoverAccountRepo) ListSchedulableByPlatform(_ context.Cont
return accounts, nil
}
func (r codexModelsFailoverAccountRepo) ListSchedulableByGroupID(_ context.Context, _ int64) ([]service.Account, error) {
return append([]service.Account(nil), r.accounts...), nil
}
type codexModelsFailoverHTTPUpstream struct {
service.HTTPUpstream
mu sync.Mutex
@@ -116,6 +121,79 @@ func TestCodexModelsCanceledRequestDoesNotWriteResponse(t *testing.T) {
}
}
func TestCodexModelsAppliesLocalFiltersBeforeClientETag(t *testing.T) {
gin.SetMode(gin.TestMode)
groupID := int64(43)
repo := &codexModelsFailoverAccountRepo{accounts: []service.Account{
{
ID: 1,
Name: "custom-openai",
Platform: service.PlatformOpenAI,
Type: service.AccountTypeAPIKey,
Status: service.StatusActive,
Schedulable: true,
Concurrency: 1,
Credentials: map[string]any{
"api_key": "sk-test",
"base_url": "https://upstream.example/v1",
"model_mapping": map[string]any{
"codex-auto-review": "codex-auto-review",
},
},
},
}}
upstream := &codexModelsFailoverHTTPUpstream{
firstBody: `{"object":"list","data":[{"id":"codex-auto-review"},{"id":"gpt-5.6"}]}`,
}
gatewayService := service.NewOpenAIGatewayService(
repo,
nil, nil, nil, nil, nil, nil, &config.Config{RunMode: config.RunModeSimple}, nil, nil, nil, nil, nil,
upstream,
nil, nil, nil, nil, nil, nil, nil, nil,
)
handler := &OpenAIGatewayHandler{gatewayService: gatewayService}
group := &service.Group{
ID: groupID,
Platform: service.PlatformOpenAI,
ModelsListConfig: service.GroupModelsListConfig{
Enabled: true,
Models: []string{"codex-auto-review", "gpt-5.6"},
},
}
first := performCodexModelsRequestForGroup(t, handler, group, "")
if first.Code != http.StatusOK {
t.Fatalf("first status: got %d, want %d; body=%s", first.Code, http.StatusOK, first.Body.String())
}
if body := first.Body.String(); !strings.Contains(body, "codex-auto-review") || !strings.Contains(body, "gpt-5.6") {
t.Fatalf("first body did not include the explicitly selected models: %s", body)
}
oldETag := first.Header().Get("ETag")
if oldETag == "" {
t.Fatal("first response did not include an ETag")
}
group.ModelsListConfig.Enabled = false
second := performCodexModelsRequestForGroup(t, handler, group, oldETag)
if second.Code != http.StatusOK {
t.Fatalf("second status: got %d, want %d; body=%s", second.Code, http.StatusOK, second.Body.String())
}
if body := second.Body.String(); strings.Contains(body, "codex-auto-review") || !strings.Contains(body, "gpt-5.6") {
t.Fatalf("second body was not the filtered manifest: %s", body)
}
if newETag := second.Header().Get("ETag"); newETag == "" || newETag == oldETag {
t.Fatalf("second ETag: got %q, want a new final-body ETag", newETag)
}
third := performCodexModelsRequestForGroup(t, handler, group, second.Header().Get("ETag"))
if third.Code != http.StatusNotModified {
t.Fatalf("third status: got %d, want %d; body=%s", third.Code, http.StatusNotModified, third.Body.String())
}
if third.Body.Len() != 0 {
t.Fatalf("third body: got %q, want empty", third.Body.String())
}
}
func TestCompositeCodexModelsReusesExistingManifestSelection(t *testing.T) {
handler, upstream, groupID := newCodexModelsFailoverTestHandler(http.StatusServiceUnavailable)
@@ -148,9 +226,7 @@ func TestCodexModelsFailsOverFromRetryableUpstreamStatus(t *testing.T) {
if recorder.Code != http.StatusOK {
t.Fatalf("status: got %d, want %d; body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
}
if got, want := recorder.Body.String(), `{"models":[{"slug":"gpt-5.6-sol"}]}`; got != want {
t.Fatalf("body: got %q, want %q", got, want)
}
requireCompleteCodexModelsHandlerResponse(t, recorder, "gpt-5.6-sol")
})
}
}
@@ -183,9 +259,7 @@ func TestCodexModelsFailsOverFromInvalidManifestEnvelope(t *testing.T) {
if recorder.Code != http.StatusOK {
t.Fatalf("status: got %d, want %d; body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
}
if got, want := recorder.Body.String(), `{"models":[{"slug":"gpt-5.6-sol"}]}`; got != want {
t.Fatalf("body: got %q, want %q", got, want)
}
requireCompleteCodexModelsHandlerResponse(t, recorder, "gpt-5.6-sol")
}
func TestCodexModelsDoesNotFailOverFromPermanentUpstreamStatus(t *testing.T) {
@@ -300,23 +374,61 @@ func newCodexModelsFailoverTestHandlerWithAccountCount(firstStatus, accountCount
}
func performCodexModelsRequest(t *testing.T, handler *OpenAIGatewayHandler, groupID int64) *httptest.ResponseRecorder {
return performCodexModelsRequestForPlatform(t, handler, groupID, service.PlatformOpenAI)
return performCodexModelsRequestForGroup(t, handler, &service.Group{ID: groupID, Platform: service.PlatformOpenAI}, "")
}
func performCodexModelsRequestForPlatform(t *testing.T, handler *OpenAIGatewayHandler, groupID int64, platform string) *httptest.ResponseRecorder {
return performCodexModelsRequestForGroup(t, handler, &service.Group{ID: groupID, Platform: platform}, "")
}
func performCodexModelsRequestForGroup(t *testing.T, handler *OpenAIGatewayHandler, group *service.Group, etag string) *httptest.ResponseRecorder {
t.Helper()
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodGet, "/v1/models?client_version=0.144.0", nil)
if etag != "" {
c.Request.Header.Set("If-None-Match", etag)
}
c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{
GroupID: &groupID,
Group: &service.Group{ID: groupID, Platform: platform},
GroupID: &group.ID,
Group: group,
})
handler.CodexModels(c)
return recorder
}
func requireCompleteCodexModelsHandlerResponse(t *testing.T, recorder *httptest.ResponseRecorder, slug string) {
t.Helper()
var envelope struct {
Models []map[string]any `json:"models"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode body: %v; body=%s", err, recorder.Body.String())
}
if len(envelope.Models) != 1 {
t.Fatalf("models count: got %d, want 1; body=%s", len(envelope.Models), recorder.Body.String())
}
model := envelope.Models[0]
if got := model["slug"]; got != slug {
t.Fatalf("slug: got %v, want %q", got, slug)
}
if levels, ok := model["supported_reasoning_levels"].([]any); !ok || len(levels) == 0 {
t.Fatalf("supported_reasoning_levels must be populated: %v", model["supported_reasoning_levels"])
}
if messages, ok := model["model_messages"].(map[string]any); !ok || messages["instructions_template"] == "" {
t.Fatalf("model_messages.instructions_template must be populated: %v", model["model_messages"])
}
if policy, ok := model["truncation_policy"].(map[string]any); !ok || len(policy) == 0 {
t.Fatalf("truncation_policy must be populated: %v", model["truncation_policy"])
}
modalities, ok := model["input_modalities"].([]any)
if !ok || len(modalities) != 1 || modalities[0] != "text" {
t.Fatalf("custom OpenAI-compatible endpoint modalities: got %v, want [text]", model["input_modalities"])
}
}
func equalInt64Slices(got, want []int64) bool {
if len(got) != len(want) {
return false
@@ -0,0 +1,69 @@
package claude
import (
"strings"
"unicode"
)
var (
effortLowMediumHigh = []string{"low", "medium", "high"}
effortLowMediumHighMax = []string{"low", "medium", "high", "max"}
effortLowMediumHighXHighMax = []string{"low", "medium", "high", "xhigh", "max"}
)
var effortFamilies = []struct {
family string
levels []string
}{
{family: "claude-mythos-preview", levels: effortLowMediumHighMax},
{family: "claude-mythos-5", levels: effortLowMediumHighXHighMax},
{family: "claude-fable-5", levels: effortLowMediumHighXHighMax},
{family: "claude-sonnet-4-6", levels: effortLowMediumHighMax},
{family: "claude-sonnet-5", levels: effortLowMediumHighXHighMax},
{family: "claude-opus-4-8", levels: effortLowMediumHighXHighMax},
{family: "claude-opus-4-7", levels: effortLowMediumHighXHighMax},
{family: "claude-opus-4-6", levels: effortLowMediumHighMax},
{family: "claude-opus-4-5", levels: effortLowMediumHigh},
{family: "claude-opus-5", levels: effortLowMediumHighXHighMax},
}
// EffortLevelsForModel returns the output_config.effort values accepted by a
// Claude model, ordered from the lightest to the deepest reasoning level.
func EffortLevelsForModel(model string) []string {
id := normalizeEffortModelID(model)
for _, entry := range effortFamilies {
if id == entry.family || strings.HasPrefix(id, entry.family+"-") {
return append([]string(nil), entry.levels...)
}
}
return nil
}
func normalizeEffortModelID(model string) string {
id := strings.ToLower(strings.TrimSpace(model))
id = strings.TrimPrefix(id, "models/")
if slash := strings.IndexByte(id, '/'); slash >= 0 {
id = strings.TrimPrefix(strings.TrimSpace(id[slash+1:]), "models/")
}
id = strings.TrimPrefix(id, "anthropic.")
id = strings.TrimSuffix(id, "-thinking")
if mapped, ok := ModelIDReverseOverrides[id]; ok {
id = mapped
}
if len(id) >= 9 {
suffix := id[len(id)-9:]
if suffix[0] == '-' {
digits := true
for _, r := range suffix[1:] {
if !unicode.IsDigit(r) {
digits = false
break
}
}
if digits {
id = id[:len(id)-9]
}
}
}
return id
}
@@ -0,0 +1,29 @@
package claude
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestEffortLevelsForModel(t *testing.T) {
t.Parallel()
tests := []struct {
model string
want []string
}{
{model: "claude-opus-4-6", want: []string{"low", "medium", "high", "max"}},
{model: "anthropic/claude-sonnet-4-6", want: []string{"low", "medium", "high", "max"}},
{model: "claude-opus-5", want: []string{"low", "medium", "high", "xhigh", "max"}},
{model: "claude-opus-4-5-20251101", want: []string{"low", "medium", "high"}},
{model: "claude-haiku-4-5-20251001", want: nil},
{model: "gpt-5.6", want: nil},
}
for _, tt := range tests {
t.Run(tt.model, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.want, EffortLevelsForModel(tt.model))
})
}
}
+24
View File
@@ -251,6 +251,30 @@ func IsGrokModelID(model string) bool {
return false
}
// IsGrokImagineModel reports whether model is a Grok Imagine image or video
// model. These media models cannot act as the primary Codex agent model.
func IsGrokImagineModel(model string) bool {
normalized := strings.ToLower(StripGrokProviderPrefix(model))
if normalized == "" {
return false
}
if strings.HasPrefix(normalized, "imagine") {
return true
}
switch {
case normalized == "grok-imagine",
normalized == "grok-imagine-1",
normalized == "grok-imagine-edit",
normalized == "grok-video-1.5":
return true
case strings.HasPrefix(normalized, "grok-imagine-image"),
strings.HasPrefix(normalized, "grok-imagine-video"):
return true
default:
return false
}
}
// IsGrokTextResponsesModelID reports whether model is a known Grok text model
// for the Responses API. Imagine image/video and unknown custom ids return false.
func IsGrokTextResponsesModelID(model string) bool {
+10
View File
@@ -58,6 +58,16 @@ func TestIsGrokModelID(t *testing.T) {
require.False(t, IsGrokModelID("claude-sonnet-4"))
}
func TestIsGrokImagineModel(t *testing.T) {
t.Parallel()
require.True(t, IsGrokImagineModel("grok-imagine-image"))
require.True(t, IsGrokImagineModel("grok-imagine-video-1.5-preview"))
require.True(t, IsGrokImagineModel("xai/grok-imagine-image-quality"))
require.True(t, IsGrokImagineModel("grok-video-1.5"))
require.False(t, IsGrokImagineModel("grok-4.6"))
require.False(t, IsGrokImagineModel("grok-build-0.1"))
}
func TestDefaultModelsIncludesGrok46(t *testing.T) {
t.Parallel()
ids := DefaultModelIDs()
File diff suppressed because it is too large Load Diff
@@ -24,32 +24,766 @@ import (
"golang.org/x/net/http2"
)
// Scenario: non-OpenAI groups expose their effective model IDs as a Codex manifest.
func TestBuildCodexModelsManifest(t *testing.T) {
body, err := BuildCodexModelsManifest([]string{
" grok-4.6 ",
"deepseek-v4-pro",
"grok-4.6",
"",
})
require.NoError(t, err)
var got struct {
Models []struct {
Slug string `json:"slug"`
} `json:"models"`
}
require.NoError(t, json.Unmarshal(body, &got))
require.Equal(t, []string{"grok-4.6", "deepseek-v4-pro"}, []string{
got.Models[0].Slug,
got.Models[1].Slug,
})
}
type codexModelsHTTPUpstreamStub struct {
do func(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error)
}
type codexModelsVisibilityAccountRepo struct {
AccountRepository
byGroup map[int64][]Account
}
func (r codexModelsVisibilityAccountRepo) ListSchedulableByGroupID(_ context.Context, groupID int64) ([]Account, error) {
accounts := r.byGroup[groupID]
return append([]Account(nil), accounts...), nil
}
type countingCodexModelsAccountRepo struct {
AccountRepository
accounts []Account
err error
calls atomic.Int32
}
func (r *countingCodexModelsAccountRepo) ListSchedulableByGroupID(_ context.Context, _ int64) ([]Account, error) {
r.calls.Add(1)
if r.err != nil {
return nil, r.err
}
return append([]Account(nil), r.accounts...), nil
}
func decodeCodexManifestModels(t *testing.T, body []byte) []map[string]any {
t.Helper()
var envelope struct {
Models []map[string]any `json:"models"`
}
require.NoError(t, json.Unmarshal(body, &envelope))
return envelope.Models
}
func codexManifestModelSlugs(t *testing.T, body []byte) []string {
t.Helper()
models := decodeCodexManifestModels(t, body)
slugs := make([]string, 0, len(models))
for _, model := range models {
slug, ok := model["slug"].(string)
require.True(t, ok)
slugs = append(slugs, slug)
}
return slugs
}
func requireCompleteConfiguredCodexModel(t *testing.T, model map[string]any, slug string) {
t.Helper()
require.Equal(t, slug, model["slug"])
require.NotEmpty(t, model["display_name"])
require.NotEmpty(t, model["description"])
require.Equal(t, "unified_exec", model["shell_type"])
require.Equal(t, "list", model["visibility"])
require.Equal(t, true, model["supported_in_api"])
require.NotNil(t, model["priority"])
require.Equal(t, []any{}, model["additional_speed_tiers"])
require.Equal(t, []any{}, model["service_tiers"])
require.Contains(t, model, "default_service_tier")
require.Contains(t, model, "availability_nux")
require.Contains(t, model, "upgrade")
require.Contains(t, model, "default_verbosity")
require.Contains(t, model, "apply_patch_tool_type")
require.Contains(t, model, "auto_compact_token_limit")
require.Contains(t, model, "comp_hash")
require.Contains(t, model, "auto_review_model_override")
require.Contains(t, model, "model_specialty")
require.Contains(t, model, "tool_mode")
require.Contains(t, model, "multi_agent_version")
require.Equal(t, true, model["supports_reasoning_summary_parameter"])
require.Contains(t, model, "include_skills_usage_instructions")
require.Contains(t, model, "include_plugin_usage_instructions")
require.Contains(t, model, "include_apps_usage_instructions")
require.Contains(t, model, "supports_image_detail_original")
require.Contains(t, model, "node_repl_auto_review_required")
require.Contains(t, model, "node_repl_disabled")
require.Contains(t, model, "truncation_policy")
require.Contains(t, model, "supports_parallel_tool_calls")
require.Contains(t, model, "experimental_supported_tools")
modelMessages, ok := model["model_messages"].(map[string]any)
require.True(t, ok)
require.NotEmpty(t, modelMessages["instructions_template"])
for _, key := range []string{
"instructions_variables",
"approvals",
"collaboration_modes",
"auto_review",
"permissions",
"multi_agent",
"token_budget",
"guardian_v2",
} {
require.Contains(t, modelMessages, key)
}
}
func effortsFromManifestModel(t *testing.T, model map[string]any) []string {
t.Helper()
levels, ok := model["supported_reasoning_levels"].([]any)
require.True(t, ok)
efforts := make([]string, 0, len(levels))
for _, rawLevel := range levels {
level, ok := rawLevel.(map[string]any)
require.True(t, ok)
effort, ok := level["effort"].(string)
require.True(t, ok)
efforts = append(efforts, effort)
}
return efforts
}
func TestNewConfiguredCodexModelDescriptorUsesProviderMetadataAndSafeFallback(t *testing.T) {
t.Parallel()
deepSeek := newConfiguredCodexModelDescriptor("deepseek-v4-pro")
require.Equal(t, "DeepSeek V4 Pro", deepSeek.DisplayName)
require.Equal(t, int64(1_000_000), deepSeek.ContextWindow)
require.Equal(t, int64(1_000_000), deepSeek.MaxContextWindow)
require.NotNil(t, deepSeek.DefaultReasoningLevel)
require.Equal(t, "high", *deepSeek.DefaultReasoningLevel)
require.Equal(t, []configuredCodexReasoningLevel{
{Effort: "low", Description: "Fast responses with lighter reasoning"},
{Effort: "high", Description: "Greater reasoning depth for coding and agent tasks"},
{Effort: "max", Description: "Maximum reasoning depth for complex tasks"},
}, deepSeek.SupportedReasoningLevels)
require.True(t, deepSeek.SupportsParallelToolCalls)
require.Equal(t, []string{"text"}, deepSeek.InputModalities)
grok := newConfiguredCodexModelDescriptor("grok-4.6")
require.Equal(t, "Grok 4.6", grok.DisplayName)
require.Equal(t, int64(500_000), grok.ContextWindow)
require.Equal(t, int64(500_000), grok.MaxContextWindow)
require.NotNil(t, grok.DefaultReasoningLevel)
require.Equal(t, "high", *grok.DefaultReasoningLevel)
require.Equal(t, []string{"low", "medium", "high", "xhigh"}, effortsFromConfiguredCodexLevels(grok.SupportedReasoningLevels))
require.True(t, grok.SupportsParallelToolCalls)
require.Equal(t, []string{"text"}, grok.InputModalities)
require.NotContains(t, grok.SupportedReasoningLevels, configuredCodexReasoningLevel{Effort: "none"})
require.NotContains(t, grok.SupportedReasoningLevels, configuredCodexReasoningLevel{Effort: "max"})
grokAlias := newConfiguredCodexModelDescriptor("xai/grok-4.6-latest")
require.Equal(t, "Grok 4.6", grokAlias.DisplayName)
require.NotNil(t, grokAlias.DefaultReasoningLevel)
require.Equal(t, "high", *grokAlias.DefaultReasoningLevel)
require.Equal(t, []string{"low", "medium", "high", "xhigh"}, effortsFromConfiguredCodexLevels(grokAlias.SupportedReasoningLevels))
grok45 := newConfiguredCodexModelDescriptor("grok-4.5")
require.Equal(t, []string{"low", "medium", "high"}, effortsFromConfiguredCodexLevels(grok45.SupportedReasoningLevels))
grokNonReasoning := newConfiguredCodexModelDescriptor("grok-4.20-0309-non-reasoning")
require.Equal(t, "Grok 4.20 Non Reasoning", grokNonReasoning.DisplayName)
require.Nil(t, grokNonReasoning.DefaultReasoningLevel)
require.Empty(t, grokNonReasoning.SupportedReasoningLevels)
claude := newConfiguredCodexModelDescriptor("claude-opus-4-6")
require.Equal(t, "Claude Opus 4.6", claude.DisplayName)
require.NotNil(t, claude.DefaultReasoningLevel)
require.Equal(t, "medium", *claude.DefaultReasoningLevel)
require.Equal(t, []string{"low", "medium", "high", "max"}, effortsFromConfiguredCodexLevels(claude.SupportedReasoningLevels))
require.NotContains(t, claude.SupportedReasoningLevels, configuredCodexReasoningLevel{Effort: "xhigh"})
require.NotContains(t, claude.SupportedReasoningLevels, configuredCodexReasoningLevel{Effort: "none"})
require.True(t, claude.SupportsParallelToolCalls)
claudeOpus5 := newConfiguredCodexModelDescriptor("claude-opus-5")
require.Equal(t, []string{"low", "medium", "high", "xhigh", "max"}, effortsFromConfiguredCodexLevels(claudeOpus5.SupportedReasoningLevels))
providerQualifiedClaude := newConfiguredCodexModelDescriptor("anthropic/claude-sonnet-4-6")
require.Equal(t, "Claude Sonnet 4.6", providerQualifiedClaude.DisplayName)
require.Equal(t, []string{"low", "medium", "high", "max"}, effortsFromConfiguredCodexLevels(providerQualifiedClaude.SupportedReasoningLevels))
claudeHaiku := newConfiguredCodexModelDescriptor("claude-haiku-4-5-20251001")
require.Equal(t, "Claude Haiku 4.5", claudeHaiku.DisplayName)
require.Nil(t, claudeHaiku.DefaultReasoningLevel)
require.Empty(t, claudeHaiku.SupportedReasoningLevels)
gpt56 := newConfiguredCodexModelDescriptor("gpt-5.6-sol")
require.Equal(t, "GPT-5.6 Sol", gpt56.DisplayName)
require.Equal(t, "OpenAI GPT coding model routed through Sub2API.", gpt56.Description)
require.NotNil(t, gpt56.DefaultReasoningLevel)
require.Equal(t, "low", *gpt56.DefaultReasoningLevel)
require.Equal(t, configuredCodexGPTReasoningLevels("gpt-5.6-sol"), gpt56.SupportedReasoningLevels)
require.Equal(t, []string{"low", "medium", "high", "xhigh", "max", "ultra"}, effortsFromConfiguredCodexLevels(gpt56.SupportedReasoningLevels))
require.True(t, gpt56.SupportsParallelToolCalls)
require.True(t, gpt56.SupportVerbosity)
require.Equal(t, []string{"text"}, gpt56.InputModalities)
require.Equal(t, int64(872_000), gpt56.MaxContextWindow)
require.Equal(t, configuredCodexTruncationPolicy{Mode: "tokens", Limit: 10_000}, gpt56.TruncationPolicy)
require.NotNil(t, gpt56.DefaultVerbosity)
require.Equal(t, "low", *gpt56.DefaultVerbosity)
require.True(t, gpt56.SupportsReasoningSummaryParameter)
require.Equal(t, "none", gpt56.DefaultReasoningSummary)
gpt56Luna := newConfiguredCodexModelDescriptor("gpt-5.6-luna")
require.Equal(t, []string{"low", "medium", "high", "xhigh", "max"}, effortsFromConfiguredCodexLevels(gpt56Luna.SupportedReasoningLevels))
require.Equal(t, "medium", *gpt56Luna.DefaultReasoningLevel)
gpt55 := newConfiguredCodexModelDescriptor("gpt-5.5")
require.Equal(t, "GPT-5.5", gpt55.DisplayName)
require.NotNil(t, gpt55.DefaultReasoningLevel)
require.Equal(t, "medium", *gpt55.DefaultReasoningLevel)
require.Equal(t, []string{"low", "medium", "high", "xhigh"}, effortsFromConfiguredCodexLevels(gpt55.SupportedReasoningLevels))
require.NotContains(t, gpt55.SupportedReasoningLevels, configuredCodexReasoningLevel{Effort: "max"})
require.NotNil(t, gpt55.DefaultVerbosity)
require.Equal(t, "low", *gpt55.DefaultVerbosity)
gpt4o := newConfiguredCodexModelDescriptor("gpt-4o")
require.Equal(t, "gpt-4o", gpt4o.DisplayName)
require.Nil(t, gpt4o.DefaultReasoningLevel)
require.Empty(t, gpt4o.SupportedReasoningLevels)
require.True(t, gpt4o.SupportsParallelToolCalls)
image := newConfiguredCodexModelDescriptor("gpt-image-2")
require.Equal(t, "gpt-image-2", image.DisplayName)
require.Nil(t, image.DefaultReasoningLevel)
require.Empty(t, image.SupportedReasoningLevels)
custom := newConfiguredCodexModelDescriptor("company-coding-model")
require.Equal(t, "company-coding-model", custom.DisplayName)
require.Equal(t, int64(272_000), custom.ContextWindow)
require.Nil(t, custom.DefaultReasoningLevel)
require.Empty(t, custom.SupportedReasoningLevels)
require.False(t, custom.SupportsParallelToolCalls)
require.NotEmpty(t, custom.ModelMessages.InstructionsTemplate)
require.Equal(t, "auto", custom.DefaultReasoningSummary)
require.Equal(t, configuredCodexTruncationPolicy{Mode: "bytes", Limit: 10_000}, custom.TruncationPolicy)
}
func effortsFromConfiguredCodexLevels(levels []configuredCodexReasoningLevel) []string {
efforts := make([]string, 0, len(levels))
for _, level := range levels {
efforts = append(efforts, level.Effort)
}
return efforts
}
// Scenario: 专用图片生成模型不进入 Codex 主模型目录。
func TestBuildCodexModelsManifestOmitsDedicatedImageModels(t *testing.T) {
t.Parallel()
body, err := BuildCodexModelsManifest([]string{
"grok-4.6",
"gpt-image-1",
"gpt-image-1.5",
"gpt-image-2",
"openai/gpt-image-2",
"gemini-2.5-flash-image",
"gemini-3.1-flash-image-preview",
"gemini-3-pro-image",
"google/gemini-3-pro-image",
"google/models/gemini-2.5-flash-image-preview",
"grok-imagine-image",
"grok-imagine-video",
"xai/grok-imagine-image-quality",
"grok-4.5",
})
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
slugs := make([]string, 0, len(models))
for _, model := range models {
slug, _ := model["slug"].(string)
slugs = append(slugs, slug)
}
require.Equal(t, []string{"grok-4.6", "grok-4.5"}, slugs)
}
func TestBuildCodexModelsManifestForGroupAdvertisesOfficialGrokResponsesImageInput(t *testing.T) {
t.Parallel()
const groupID int64 = 701
svc := &GatewayService{
accountRepo: codexModelsVisibilityAccountRepo{byGroup: map[int64][]Account{
groupID: {{
ID: 1,
Platform: PlatformGrok,
Type: AccountTypeOAuth,
Credentials: map[string]any{
"access_token": "token",
},
}},
}},
}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: groupID, Platform: PlatformComposite},
"",
[]string{"grok-4.5"},
)
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, []any{"text", "image"}, models[0]["input_modalities"])
}
func TestBuildCodexModelsManifestForGroupAdvertisesOfficialOpenAIResponsesImageInput(t *testing.T) {
t.Parallel()
const groupID int64 = 702
svc := &GatewayService{
accountRepo: codexModelsVisibilityAccountRepo{byGroup: map[int64][]Account{
groupID: {{
ID: 2,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
}},
}},
}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: groupID, Platform: PlatformComposite},
"",
[]string{"gpt-5.6-sol"},
)
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, []any{"text", "image"}, models[0]["input_modalities"])
}
func TestBuildCodexModelsManifestForGroupUsesConservativeProviderImageCapabilities(t *testing.T) {
t.Parallel()
tests := []struct {
name string
model string
accounts []Account
modalities []any
}{
{
name: "official Grok 4.6",
model: "grok-4.6",
accounts: []Account{{
ID: 10, Platform: PlatformGrok, Type: AccountTypeOAuth,
}},
modalities: []any{"text", "image"},
},
{
name: "official Grok Build vision host",
model: "grok-build-0.1",
accounts: []Account{{
ID: 11, Platform: PlatformGrok, Type: AccountTypeOAuth,
}},
modalities: []any{"text", "image"},
},
{
name: "official Grok 4.20 vision model",
model: "grok-4.20-0309-reasoning",
accounts: []Account{{
ID: 23, Platform: PlatformGrok, Type: AccountTypeOAuth,
}},
modalities: []any{"text", "image"},
},
{
name: "Grok 3 Mini is text only",
model: "grok-3-mini",
accounts: []Account{{
ID: 24, Platform: PlatformGrok, Type: AccountTypeOAuth,
}},
modalities: []any{"text"},
},
{
name: "Grok Composer has only Chat image bridge",
model: "grok-composer-2.5-fast",
accounts: []Account{{
ID: 12, Platform: PlatformGrok, Type: AccountTypeOAuth,
}},
modalities: []any{"text"},
},
{
name: "custom Grok host",
model: "grok-4.5",
accounts: []Account{{
ID: 13, Platform: PlatformGrok, Type: AccountTypeAPIKey,
Credentials: map[string]any{"base_url": "https://relay.example.test/v1"},
}},
modalities: []any{"text"},
},
{
name: "malformed Grok host",
model: "grok-4.5",
accounts: []Account{{
ID: 19, Platform: PlatformGrok, Type: AccountTypeAPIKey,
Credentials: map[string]any{"base_url": "::invalid::url"},
}},
modalities: []any{"text"},
},
{
name: "mixed official and custom Grok candidates",
model: "grok-4.5",
accounts: []Account{
{ID: 14, Platform: PlatformGrok, Type: AccountTypeOAuth},
{
ID: 15, Platform: PlatformGrok, Type: AccountTypeAPIKey,
Credentials: map[string]any{"base_url": "https://relay.example.test/v1"},
},
},
modalities: []any{"text"},
},
{
name: "DeepSeek V4",
model: "deepseek-v4-pro",
accounts: []Account{{
ID: 16, Platform: PlatformDeepseek, Type: AccountTypeAPIKey,
}},
modalities: []any{"text"},
},
{
name: "official OpenAI API key",
model: "gpt-5.6-sol",
accounts: []Account{{
ID: 17, Platform: PlatformOpenAI, Type: AccountTypeAPIKey,
}},
modalities: []any{"text", "image"},
},
{
name: "official OpenAI legacy text model",
model: "gpt-3.5-turbo",
accounts: []Account{{
ID: 20, Platform: PlatformOpenAI, Type: AccountTypeAPIKey,
}},
modalities: []any{"text"},
},
{
name: "custom OpenAI-compatible host",
model: "gpt-5.6-sol",
accounts: []Account{{
ID: 18, Platform: PlatformOpenAI, Type: AccountTypeAPIKey,
Credentials: map[string]any{"base_url": "https://openai-compatible.example.test/v1"},
}},
modalities: []any{"text"},
},
}
for i, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
groupID := int64(710 + i)
svc := &GatewayService{accountRepo: codexModelsVisibilityAccountRepo{byGroup: map[int64][]Account{
groupID: tt.accounts,
}}}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: groupID, Platform: PlatformComposite},
"",
[]string{tt.model},
)
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, tt.modalities, models[0]["input_modalities"])
})
}
}
func TestBuildCodexModelsManifestForGroupUsesExplicitCompositeResponsesRouteModel(t *testing.T) {
t.Parallel()
const groupID int64 = 730
routeRepo := compositeRouteRepoStub{routes: []CompositeModelRoute{{
ID: 1,
GroupID: groupID,
PublicModel: "vision-alias",
MatchType: CompositeRouteMatchExact,
TargetPlatform: PlatformGrok,
UpstreamModel: "grok-4.5",
Endpoint: CompositeRouteEndpointResponses,
Enabled: true,
}}}
svc := &GatewayService{
accountRepo: codexModelsVisibilityAccountRepo{byGroup: map[int64][]Account{
groupID: {{ID: 20, Platform: PlatformGrok, Type: AccountTypeOAuth}},
}},
compositeResolver: NewCompositeRouteResolver(routeRepo),
}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: groupID, Platform: PlatformComposite},
"",
[]string{"vision-alias"},
)
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, []any{"text", "image"}, models[0]["input_modalities"])
}
func TestBuildCodexModelsManifestForGroupUsesAccountMappingOwnershipAndMappedModel(t *testing.T) {
t.Parallel()
const groupID int64 = 731
svc := &GatewayService{accountRepo: codexModelsVisibilityAccountRepo{byGroup: map[int64][]Account{
groupID: {{
ID: 21,
Platform: PlatformGrok,
Type: AccountTypeOAuth,
Credentials: map[string]any{
"model_mapping": map[string]any{"vision-alias": "grok-4.5"},
},
}},
}}}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: groupID, Platform: PlatformComposite},
"",
[]string{"vision-alias"},
)
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, []any{"text", "image"}, models[0]["input_modalities"])
}
func TestBuildCodexModelsManifestForGroupLoadsAccountsOnce(t *testing.T) {
t.Parallel()
const groupID int64 = 732
repo := &countingCodexModelsAccountRepo{accounts: []Account{{
ID: 22,
Platform: PlatformGrok,
Type: AccountTypeOAuth,
Credentials: map[string]any{
"model_mapping": map[string]any{
"vision-alias-a": "grok-4.5",
"vision-alias-b": "grok-4.6",
},
},
}}}
svc := &GatewayService{accountRepo: repo}
_, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: groupID, Platform: PlatformComposite},
"",
[]string{"vision-alias-a", "vision-alias-b", "deepseek-v4-pro"},
)
require.NoError(t, err)
require.Equal(t, int32(1), repo.calls.Load())
}
func TestBuildCodexModelsManifestForGroupSkipsCapabilityLookupForTextOnlyPlatform(t *testing.T) {
t.Parallel()
repo := &countingCodexModelsAccountRepo{}
svc := &GatewayService{accountRepo: repo}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: 733, Platform: PlatformDeepseek},
"",
[]string{"deepseek-v4-pro"},
)
require.NoError(t, err)
require.Equal(t, int32(0), repo.calls.Load())
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, []any{"text"}, models[0]["input_modalities"])
}
func TestBuildCodexModelsManifestForGroupFallsBackWhenCapabilityLookupFails(t *testing.T) {
t.Parallel()
repo := &countingCodexModelsAccountRepo{err: errors.New("account repository unavailable")}
svc := &GatewayService{accountRepo: repo}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: 734, Platform: PlatformComposite},
"",
[]string{"gpt-5.6-sol", "grok-4.5"},
)
require.NoError(t, err)
require.Equal(t, int32(1), repo.calls.Load())
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 2)
require.Equal(t, []any{"text"}, models[0]["input_modalities"])
require.Equal(t, []any{"text"}, models[1]["input_modalities"])
}
func TestMergeGroupConfiguredCodexModelsInjectsCurrentGroupAliases(t *testing.T) {
t.Parallel()
const groupID int64 = 71
svc := &OpenAIGatewayService{accountRepo: codexModelsVisibilityAccountRepo{
byGroup: map[int64][]Account{
groupID: {
{
Platform: PlatformOpenAI,
Credentials: map[string]any{
"model_mapping": map[string]any{
"deepseek-4-pro": "deepseek-v4-pro",
},
},
},
},
72: {
{
Platform: PlatformOpenAI,
Credentials: map[string]any{
"model_mapping": map[string]any{"other-group-model": "upstream-model"},
},
},
},
},
}}
manifest := &CodexModelsManifest{
Body: []byte(`{"models":[{"slug":"gpt-5.6","display_name":"GPT-5.6","unknown":{"kept":true}}],"metadata":{"version":1}}`),
}
err := svc.MergeGroupConfiguredCodexModels(
context.Background(),
&Group{ID: groupID, Platform: PlatformOpenAI},
manifest,
"",
)
require.NoError(t, err)
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 2)
require.Equal(t, "gpt-5.6", models[0]["slug"])
require.Equal(t, map[string]any{"kept": true}, models[0]["unknown"])
requireCompleteConfiguredCodexModel(t, models[1], "deepseek-4-pro")
require.EqualValues(t, 1_000_000, models[1]["context_window"])
require.EqualValues(t, 1_000_000, models[1]["max_context_window"])
require.Equal(t, "high", models[1]["default_reasoning_level"])
require.Len(t, models[1]["supported_reasoning_levels"], 3)
require.NotContains(t, string(manifest.Body), "other-group-model")
require.Equal(t, codexModelsManifestBodyETag(manifest.Body), manifest.ETag)
}
func TestMergeGroupConfiguredCodexModelsFiltersAutoReviewByDefault(t *testing.T) {
t.Parallel()
const groupID int64 = 74
svc := &OpenAIGatewayService{accountRepo: codexModelsVisibilityAccountRepo{}}
manifest := &CodexModelsManifest{
Body: []byte(`{"models":[{"slug":"codex-auto-review","visibility":"list"},{"slug":"codex-auto-future","visibility":"list"},{"slug":"gpt-image-2","visibility":"list"},{"slug":"gpt-5.6","visibility":"list"}]}`),
}
require.NoError(t, svc.MergeGroupConfiguredCodexModels(
context.Background(),
&Group{ID: groupID, Platform: PlatformOpenAI},
manifest,
"",
))
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 1)
require.Equal(t, "gpt-5.6", models[0]["slug"])
require.Equal(t, codexModelsManifestBodyETag(manifest.Body), manifest.ETag)
}
// Scenario: OpenAI 账号映射不启用 Auto Review。
func TestMergeGroupConfiguredCodexModelsFiltersAccountMappedAutoReviewByDefault(t *testing.T) {
t.Parallel()
const groupID int64 = 75
svc := &OpenAIGatewayService{accountRepo: codexModelsVisibilityAccountRepo{
byGroup: map[int64][]Account{
groupID: {
{
Platform: PlatformOpenAI,
Credentials: map[string]any{
"model_mapping": map[string]any{
openai.CodexUsageProbeModel: openai.CodexUsageProbeModel,
},
},
},
},
},
}}
manifest := &CodexModelsManifest{
Body: []byte(`{"models":[{"slug":"codex-auto-review","visibility":"hide","model_messages":{"auto_review":{"enabled":true}}},{"slug":"gpt-5.6","visibility":"list"}]}`),
}
require.NoError(t, svc.MergeGroupConfiguredCodexModels(
context.Background(),
&Group{ID: groupID, Platform: PlatformOpenAI},
manifest,
"",
))
require.Equal(t, []string{"gpt-5.6"}, codexManifestModelSlugs(t, manifest.Body))
}
// Scenario: 启用的分组自定义列表允许 Auto Review。
func TestMergeGroupConfiguredCodexModelsKeepsExplicitAutoReviewSelection(t *testing.T) {
t.Parallel()
const groupID int64 = 76
svc := &OpenAIGatewayService{accountRepo: codexModelsVisibilityAccountRepo{}}
manifest := &CodexModelsManifest{
Body: []byte(`{"models":[{"slug":"codex-auto-review","visibility":"list"},{"slug":"gpt-5.6","visibility":"list"}]}`),
}
group := &Group{
ID: groupID,
Platform: PlatformOpenAI,
ModelsListConfig: GroupModelsListConfig{
Enabled: true,
Models: []string{openai.CodexUsageProbeModel},
},
}
require.NoError(t, svc.MergeGroupConfiguredCodexModels(context.Background(), group, manifest, ""))
require.Equal(t, []string{"codex-auto-review"}, codexManifestModelSlugs(t, manifest.Body))
}
func TestMergeGroupConfiguredCodexModelsHonorsCustomListAndFinalETag(t *testing.T) {
t.Parallel()
const groupID int64 = 73
svc := &OpenAIGatewayService{accountRepo: codexModelsVisibilityAccountRepo{
byGroup: map[int64][]Account{
groupID: {
{
Platform: PlatformOpenAI,
Credentials: map[string]any{
"model_mapping": map[string]any{
"deepseek-4-pro": "deepseek-v4-pro",
"hidden-alias": "hidden-upstream",
},
},
},
},
},
}}
group := &Group{
ID: groupID,
Platform: PlatformOpenAI,
ModelsListConfig: GroupModelsListConfig{
Enabled: true,
Models: []string{"deepseek-4-pro"},
},
}
upstreamBody := []byte(`{"models":[{"slug":"gpt-5.6","display_name":"GPT-5.6"}]}`)
manifest := &CodexModelsManifest{Body: upstreamBody}
require.NoError(t, svc.MergeGroupConfiguredCodexModels(context.Background(), group, manifest, ""))
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 1)
requireCompleteConfiguredCodexModel(t, models[0], "deepseek-4-pro")
finalETag := manifest.ETag
second := &CodexModelsManifest{Body: upstreamBody}
require.NoError(t, svc.MergeGroupConfiguredCodexModels(context.Background(), group, second, finalETag))
require.True(t, second.NotModified)
require.Empty(t, second.Body)
require.Equal(t, finalETag, second.ETag)
}
type codexModelsBlockingBody struct {
ctx context.Context
readStarted chan struct{}
@@ -429,7 +1163,7 @@ func TestFetchCodexModelsManifestMissingToken(t *testing.T) {
}
func TestFetchCodexModelsManifestAPIKeyCustomUpstream(t *testing.T) {
manifestBody := `{"models":[{"slug":"gpt-5.6"}]}`
manifestBody := `{"models":[{"slug":"deepseek-v4-pro"}]}`
var gotRequest *http.Request
var gotProxyURL string
var gotAccountID int64
@@ -486,12 +1220,13 @@ func TestFetchCodexModelsManifestAPIKeyCustomUpstream(t *testing.T) {
if gotProxyURL != "" || gotAccountID != 2 || gotConcurrency != 3 {
t.Errorf("upstream routing metadata: proxy=%q account_id=%d concurrency=%d", gotProxyURL, gotAccountID, gotConcurrency)
}
if string(manifest.Body) != manifestBody {
t.Errorf("body not passed through verbatim: got %q", manifest.Body)
}
if manifest.ETag != `W/"api-key-manifest"` {
t.Errorf("etag not passed through: got %q", manifest.ETag)
}
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 1)
requireCompleteConfiguredCodexModel(t, models[0], "deepseek-v4-pro")
require.Equal(t, "DeepSeek V4 Pro", models[0]["display_name"])
require.EqualValues(t, 1_000_000, models[0]["context_window"])
require.Equal(t, codexModelsManifestBodyETag(manifest.Body), manifest.ETag)
require.Equal(t, `W/"api-key-manifest"`, manifest.upstreamETag)
}
func TestFetchCodexModelsManifestAPIKeyConvertsStandardOpenAIModelList(t *testing.T) {
@@ -516,13 +1251,96 @@ func TestFetchCodexModelsManifestAPIKeyConvertsStandardOpenAIModelList(t *testin
if err != nil {
t.Fatalf("FetchCodexModelsManifest returned error: %v", err)
}
if got, want := string(manifest.Body), `{"models":[{"slug":"gpt-5.6"},{"slug":"gpt-5.6-codex"}]}`; got != want {
t.Errorf("converted body: got %q, want %q", got, want)
}
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 2)
requireCompleteConfiguredCodexModel(t, models[0], "gpt-5.6")
requireCompleteConfiguredCodexModel(t, models[1], "gpt-5.6-codex")
require.Equal(t, codexModelsManifestBodyETag(manifest.Body), manifest.ETag)
require.Equal(t, `W/"openai-list"`, manifest.upstreamETag)
}
func TestConvertOpenAIModelListToCodexManifestUsesCompleteDescriptors(t *testing.T) {
upstreamBody := `{"object":"list","data":[{"id":"gpt-5.5","object":"model"}]}`
converted := convertOpenAIModelListToCodexManifest([]byte(upstreamBody))
models := decodeCodexManifestModels(t, converted)
require.Len(t, models, 1)
requireCompleteConfiguredCodexModel(t, models[0], "gpt-5.5")
require.Equal(t, "GPT-5.5", models[0]["display_name"])
require.Equal(t, "medium", models[0]["default_reasoning_level"])
require.Len(t, models[0]["supported_reasoning_levels"], 4)
}
func TestCompleteAPIKeyCodexModelsManifestForClientPreservesProviderMetadata(t *testing.T) {
t.Parallel()
svc := &OpenAIGatewayService{}
manifest := &CodexModelsManifest{
Body: []byte(`{"models":[{"slug":"grok-4.6","description":"Provider supplied","model_messages":{"auto_review":{"enabled":true}},"truncation_policy":{"mode":"tokens"},"unknown":{"kept":true}}],"metadata":{"source":"upstream"}}`),
}
account := newCodexModelsAPIKeyTestAccount("https://upstream.example/v1")
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(manifest, account))
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 1)
requireCompleteConfiguredCodexModel(t, models[0], "grok-4.6")
require.Equal(t, "Provider supplied", models[0]["description"])
require.Equal(t, map[string]any{"kept": true}, models[0]["unknown"])
require.Equal(t, []any{"text"}, models[0]["input_modalities"])
require.Equal(t, []string{"low", "medium", "high", "xhigh"}, effortsFromManifestModel(t, models[0]))
modelMessages, ok := models[0]["model_messages"].(map[string]any)
require.True(t, ok)
require.NotEmpty(t, modelMessages["instructions_template"])
require.Equal(t, map[string]any{"enabled": true}, modelMessages["auto_review"])
require.Equal(t, map[string]any{"mode": "tokens", "limit": float64(10_000)}, models[0]["truncation_policy"])
require.Equal(t, codexModelsManifestBodyETag(manifest.Body), manifest.ETag)
var envelope map[string]any
require.NoError(t, json.Unmarshal(manifest.Body, &envelope))
require.Equal(t, map[string]any{"source": "upstream"}, envelope["metadata"])
}
func TestCompleteAPIKeyCodexModelsManifestForClientMarksOnlyOfficialVisionGPTImageInput(t *testing.T) {
t.Parallel()
svc := &OpenAIGatewayService{}
manifest := &CodexModelsManifest{Body: []byte(`{"models":[{"slug":"gpt-5.6-sol"},{"slug":"gpt-4o"},{"slug":"gpt-3.5-turbo"},{"slug":"gpt-4"}]}`)}
account := newCodexModelsAPIKeyTestAccount("")
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(manifest, account))
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 4)
bySlug := make(map[string]map[string]any, len(models))
for _, model := range models {
slug, ok := model["slug"].(string)
require.True(t, ok)
bySlug[slug] = model
}
for _, slug := range []string{"gpt-5.6-sol", "gpt-4o"} {
require.Equal(t, []any{"text", "image"}, bySlug[slug]["input_modalities"])
require.Equal(t, true, bySlug[slug]["supports_image_detail_original"])
}
for _, slug := range []string{"gpt-3.5-turbo", "gpt-4"} {
require.Equal(t, []any{"text"}, bySlug[slug]["input_modalities"])
require.Equal(t, false, bySlug[slug]["supports_image_detail_original"])
}
require.Equal(t, codexModelsManifestBodyETag(manifest.Body), manifest.ETag)
}
func TestCompleteAPIKeyCodexModelsManifestForClientFiltersOfficialNonAgentModels(t *testing.T) {
t.Parallel()
svc := &OpenAIGatewayService{}
manifest := &CodexModelsManifest{Body: []byte(`{"models":[{"slug":"gpt-5.6-sol"},{"slug":"gpt-4o-realtime-preview"},{"slug":"gpt-4o-mini-tts"},{"slug":"text-embedding-3-large"},{"slug":"omni-moderation-latest"},{"slug":"o4-mini"},{"slug":"codex-mini-latest"}]}`)}
account := newCodexModelsAPIKeyTestAccount("")
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(manifest, account))
require.Equal(t, []string{"gpt-5.6-sol", "o4-mini", "codex-mini-latest"}, codexManifestModelSlugs(t, manifest.Body))
require.Equal(t, codexModelsManifestBodyETag(manifest.Body), manifest.ETag)
}
func TestAdjustAPIKeyCodexModelsManifest(t *testing.T) {
tests := []struct {
name string
@@ -594,17 +1412,27 @@ func TestFetchCodexModelsManifestOAuthPreservesResponsesLite(t *testing.T) {
require.Equal(t, manifestBody, string(manifest.Body))
}
func TestConvertOpenAIModelListToCodexManifest(t *testing.T) {
func TestConvertOpenAIModelListToCompleteCodexManifest(t *testing.T) {
t.Parallel()
body := []byte(`{"object":"list","data":[{"id":"deepseek-v4-flash"},{"id":"deepseek-v4-pro"}]}`)
models := decodeCodexManifestModels(t, convertOpenAIModelListToCodexManifest(body))
require.Len(t, models, 2)
requireCompleteConfiguredCodexModel(t, models[0], "deepseek-v4-flash")
requireCompleteConfiguredCodexModel(t, models[1], "deepseek-v4-pro")
require.Equal(t, "DeepSeek V4 Flash", models[0]["display_name"])
require.Equal(t, "DeepSeek V4 Pro", models[1]["display_name"])
require.EqualValues(t, 1_000_000, models[0]["context_window"])
require.EqualValues(t, 1_000_000, models[1]["context_window"])
}
func TestConvertOpenAIModelListToCodexManifestLeavesUnsupportedBodiesUnchanged(t *testing.T) {
tests := []struct {
name string
body string
want string
}{
{
name: "standard list",
body: `{"object":"list","data":[{"id":"m-1"},{"id":"m-2"}]}`,
want: `{"models":[{"slug":"m-1"},{"slug":"m-2"}]}`,
},
{
name: "codex manifest unchanged",
body: `{"models":[{"slug":"m-1"}]}`,
@@ -1439,7 +2267,7 @@ func TestFetchCodexModelsManifestAPIKeyUpstreamError(t *testing.T) {
}
}
func TestFetchCodexModelsManifestAPIKeyRejectsOfficialOpenAIBaseURL(t *testing.T) {
func TestFetchCodexModelsManifestAPIKeyUsesOfficialOpenAIModelsEndpoint(t *testing.T) {
tests := []struct {
name string
baseURL string
@@ -1451,23 +2279,32 @@ func TestFetchCodexModelsManifestAPIKeyRejectsOfficialOpenAIBaseURL(t *testing.T
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := newCodexModelsAPIKeyTestService(&codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
t.Fatal("official OpenAI API key must not be used as a Codex manifest upstream")
return nil, nil
var gotURL string
s := newCodexModelsAPIKeyTestService(&codexModelsHTTPUpstreamStub{do: func(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
gotURL = req.URL.String()
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"object":"list","data":[{"id":"gpt-5.6-sol"}]}`)),
}, nil
}})
_, err := s.FetchCodexModelsManifest(
manifest, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount(tt.baseURL),
"0.144.0",
"",
)
if err == nil {
t.Fatal("expected unsupported API key upstream error, got nil")
}
if infraerrors.Reason(err) != "OPENAI_CODEX_MODELS_API_KEY_UPSTREAM_UNSUPPORTED" {
t.Errorf("error reason: got %q", infraerrors.Reason(err))
}
require.NoError(t, err)
parsedURL, parseErr := url.Parse(gotURL)
require.NoError(t, parseErr)
require.Equal(t, "api.openai.com", strings.ToLower(parsedURL.Hostname()))
require.Equal(t, "/v1/models", parsedURL.Path)
require.Equal(t, "0.144.0", parsedURL.Query().Get("client_version"))
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 1)
requireCompleteConfiguredCodexModel(t, models[0], "gpt-5.6-sol")
require.Equal(t, []any{"text", "image"}, models[0]["input_modalities"])
})
}
}
+29 -1
View File
@@ -16,10 +16,34 @@ describe('Codex models API', () => {
})
it('fetches a manifest with the current API key without adding it to the catalog', async () => {
const manifest = {
models: [
{
slug: 'grok-4.6',
default_reasoning_level: 'high',
supported_reasoning_levels: [
{ effort: 'low', description: 'Fast responses' },
{ effort: 'xhigh', description: 'Extra-high reasoning depth' }
],
input_modalities: ['text', 'image'],
model_messages: { instructions_template: 'Use the routed model.' }
},
{
slug: 'deepseek-v4-pro',
default_reasoning_level: 'high',
supported_reasoning_levels: [
{ effort: 'low', description: 'Fast responses' },
{ effort: 'max', description: 'Maximum reasoning depth' }
],
input_modalities: ['text'],
model_messages: { instructions_template: 'Use the routed model.' }
}
]
}
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ models: [{ slug: 'grok-4.6' }, { slug: 'deepseek-v4-pro' }] })
json: async () => manifest
})
vi.stubGlobal('fetch', fetchMock)
@@ -35,6 +59,10 @@ describe('Codex models API', () => {
})
)
expect(result.modelCount).toBe(2)
expect(JSON.parse(result.content)).toEqual(manifest)
expect(result.content).toContain('"effort": "xhigh"')
expect(result.content).toContain('"input_modalities"')
expect(result.content).toContain('"instructions_template"')
expect(result.content).not.toContain('sk-user-test')
})
@@ -25,6 +25,15 @@ vi.mock('file-saver', () => ({
import UseKeyModal from '../UseKeyModal.vue'
function readBlobAsText(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.addEventListener('load', () => resolve(String(reader.result || '')))
reader.addEventListener('error', () => reject(reader.error))
reader.readAsText(blob)
})
}
describe('UseKeyModal', () => {
afterEach(() => {
vi.unstubAllGlobals()
@@ -606,10 +615,28 @@ describe('UseKeyModal', () => {
// Scenario: API Key users can fetch a routed group catalog and reference it from config.toml.
it('offers a downloadable Codex catalog for Composite API keys', async () => {
const manifest = {
models: [
{
slug: 'gpt-5.5',
default_reasoning_level: 'medium',
supported_reasoning_levels: [{ effort: 'xhigh', description: 'Extra-high reasoning depth' }],
input_modalities: ['text', 'image'],
model_messages: { instructions_template: 'Use the routed model.' }
},
{
slug: 'grok-4.6',
default_reasoning_level: 'high',
supported_reasoning_levels: [{ effort: 'xhigh', description: 'Extra-high reasoning depth' }],
input_modalities: ['text'],
model_messages: { instructions_template: 'Use the routed model.' }
}
]
}
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ models: [{ slug: 'gpt-5.5' }, { slug: 'grok-4.6' }] })
json: async () => manifest
})
vi.stubGlobal('fetch', fetchMock)
@@ -663,6 +690,8 @@ describe('UseKeyModal', () => {
expect(downloadButton).toBeDefined()
await downloadButton!.trigger('click')
expect(saveAsMock).toHaveBeenCalledWith(expect.any(Blob), 'codex-models.json')
const downloadedBlob = saveAsMock.mock.calls[0]?.[0] as Blob
expect(JSON.parse(await readBlobAsText(downloadedBlob))).toEqual(manifest)
const windowsTab = wrapper.findAll('button').find((button) => button.text().trim() === 'Windows')
expect(windowsTab).toBeDefined()