Files
sub2api/backend/internal/service/openai_codex_models_service_test.go
T
lyy0709 c060aa0031 fix(models): repair tests after rebasing onto upstream main
Close the existing catalog identity test before the new capability test and
assert that an explicit null capability remains present. Update the quota
snapshot fixture to the pointer parameter already used by the current base.

Verified six full backend unit packages, scoped lint and CGO-disabled build.
2026-09-23 12:01:40 +08:00

3783 lines
134 KiB
Go

package service
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
"github.com/stretchr/testify/require"
"golang.org/x/net/http2"
)
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
}
func (r codexModelsVisibilityAccountRepo) ListModelAvailabilityCandidates(_ context.Context, groupID *int64, _ []string, _ bool) ([]Account, error) {
if groupID == nil {
return nil, nil
}
accounts := r.byGroup[*groupID]
return append([]Account(nil), accounts...), nil
}
type countingCodexModelsAccountRepo struct {
AccountRepository
accounts []Account
err error
availabilityErr error
groupID *int64
platforms []string
includeGrouped bool
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 (r *countingCodexModelsAccountRepo) ListModelAvailabilityCandidates(_ context.Context, groupID *int64, platforms []string, includeGrouped bool) ([]Account, error) {
if groupID != nil {
value := *groupID
r.groupID = &value
}
r.platforms = append([]string(nil), platforms...)
r.includeGrouped = includeGrouped
if r.availabilityErr != nil {
return nil, r.availabilityErr
}
return append([]Account(nil), r.accounts...), nil
}
type splitCodexModelsAccountRepo struct {
AccountRepository
schedulable map[int64][]Account
catalog map[int64][]Account
all map[int64][]Account
}
func (r splitCodexModelsAccountRepo) ListSchedulableByGroupID(_ context.Context, groupID int64) ([]Account, error) {
return append([]Account(nil), r.schedulable[groupID]...), nil
}
func (r splitCodexModelsAccountRepo) ListByGroup(_ context.Context, groupID int64) ([]Account, error) {
accounts := r.all[groupID]
if accounts == nil {
accounts = r.catalog[groupID]
}
return append([]Account(nil), accounts...), nil
}
func (r splitCodexModelsAccountRepo) ListModelAvailabilityCandidates(_ context.Context, groupID *int64, _ []string, _ bool) ([]Account, error) {
if groupID == nil {
return nil, nil
}
return append([]Account(nil), r.catalog[*groupID]...), nil
}
func newCodexCatalogMappedAccount(
id int64,
target string,
displayName string,
levels []string,
modalities []string,
contextWindow int64,
schedulable bool,
extraMapping map[string]any,
) Account {
reasoning := true
mapping := map[string]any{"my-coder": target}
for key, value := range extraMapping {
mapping[key] = value
}
account := Account{
ID: id,
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Status: StatusActive,
Schedulable: schedulable,
Credentials: map[string]any{
"base_url": fmt.Sprintf("https://provider-%d.example/v1", id),
"model_mapping": mapping,
},
}
models := map[string]UpstreamModelMetadata{
target: {
ID: target,
DisplayName: displayName,
Description: displayName + " upstream",
Reasoning: &reasoning,
SupportedReasoningLevels: levels,
InputModalities: modalities,
ContextWindow: contextWindow,
},
}
for _, value := range extraMapping {
exclusive, _ := value.(string)
if exclusive == "" || exclusive == target {
continue
}
models[exclusive] = UpstreamModelMetadata{
ID: exclusive,
DisplayName: "Exclusive Model",
Description: "Only mapped on the unschedulable account",
Reasoning: &reasoning,
SupportedReasoningLevels: []string{"high"},
InputModalities: []string{"text", "image"},
ContextWindow: 1_000_000,
}
}
account.SetUpstreamModelMetadataSnapshot(UpstreamModelMetadataSnapshot{Models: models})
return account
}
func TestFilterCodexModelIDsForGroupOmitsWildcardKeys(t *testing.T) {
t.Parallel()
got := FilterCodexModelIDsForGroup(
[]string{"deepseek-v4-pro", "foo-*", " bar-* ", "gpt-5.5"},
&Group{Platform: PlatformDeepseek},
)
require.Equal(t, []string{"deepseek-v4-pro", "gpt-5.5"}, got)
}
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.IsType(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.NotNil(t, grokNonReasoning.DefaultReasoningLevel)
require.Equal(t, "none", *grokNonReasoning.DefaultReasoningLevel)
require.Equal(t, []configuredCodexReasoningLevel{
{Effort: "none", Description: configuredCodexReasoningLevelDescription("none")},
}, 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.NotNil(t, claudeHaiku.DefaultReasoningLevel)
require.Equal(t, "none", *claudeHaiku.DefaultReasoningLevel)
require.Equal(t, []string{"none"}, effortsFromConfiguredCodexLevels(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)
gpt6Astra := newConfiguredCodexModelDescriptor("gpt-6-astra")
require.Equal(t, "GPT-6 Astra", gpt6Astra.DisplayName)
require.True(t, strings.HasPrefix(strings.TrimSpace(gpt6Astra.ModelMessages.InstructionsTemplate), "You are Codex, an agent based on GPT-6."))
require.NotNil(t, gpt6Astra.DefaultReasoningLevel)
require.Equal(t, "medium", *gpt6Astra.DefaultReasoningLevel)
require.Equal(t, []string{"low", "medium", "high", "xhigh", "max", "ultra"}, effortsFromConfiguredCodexLevels(gpt6Astra.SupportedReasoningLevels))
require.NotNil(t, gpt6Astra.MultiAgentReasoningEffort)
require.Equal(t, "xhigh", *gpt6Astra.MultiAgentReasoningEffort)
require.Equal(t, "v2", gpt6Astra.MultiAgentVersion)
require.NotContains(t, gpt6Astra.SupportedReasoningLevels, configuredCodexReasoningLevel{Effort: "none"})
require.True(t, configuredCodexSupportsPriorityServiceTier("gpt-6-astra"))
require.Equal(t, []configuredCodexServiceTier{{
ID: "priority",
Name: "Fast",
Description: "Priority processing for lower latency.",
}}, gpt6Astra.ServiceTiers)
require.True(t, isOpenAICodexImageInputModel("gpt-6-astra"))
require.True(t, isOpenAICodexReasoningGPTModel("openai/gpt-6-astra"))
require.True(t, isOpenAIGPT6AstraModel("gpt-6-astra-2026-09-01"))
require.False(t, isOpenAIGPT6AstraModel("gpt-6-other"))
require.Equal(t, int64(1_050_000), gpt6Astra.ContextWindow)
require.Equal(t, int64(1_050_000), gpt6Astra.MaxContextWindow)
gpt6 := newConfiguredCodexModelDescriptor("gpt-6")
require.Equal(t, "GPT-6 (Astra)", gpt6.DisplayName)
require.True(t, strings.HasPrefix(strings.TrimSpace(gpt6.ModelMessages.InstructionsTemplate), "You are Codex, an agent based on GPT-6."))
require.Equal(t, []string{"low", "medium", "high", "xhigh", "max", "ultra"}, effortsFromConfiguredCodexLevels(gpt6.SupportedReasoningLevels))
require.NotNil(t, gpt6.MultiAgentReasoningEffort)
require.Equal(t, "xhigh", *gpt6.MultiAgentReasoningEffort)
require.Equal(t, int64(1_050_000), gpt6.ContextWindow)
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.NotNil(t, gpt4o.DefaultReasoningLevel)
require.Equal(t, "none", *gpt4o.DefaultReasoningLevel)
require.Equal(t, []string{"none"}, effortsFromConfiguredCodexLevels(gpt4o.SupportedReasoningLevels))
require.True(t, gpt4o.SupportsParallelToolCalls)
image := newConfiguredCodexModelDescriptor("gpt-image-2")
require.Equal(t, "gpt-image-2", image.DisplayName)
require.NotNil(t, image.DefaultReasoningLevel)
require.Equal(t, "none", *image.DefaultReasoningLevel)
require.Equal(t, []string{"none"}, effortsFromConfiguredCodexLevels(image.SupportedReasoningLevels))
custom := newConfiguredCodexModelDescriptor("company-coding-model")
require.Equal(t, "company-coding-model", custom.DisplayName)
require.Equal(t, int64(272_000), custom.ContextWindow)
require.NotNil(t, custom.DefaultReasoningLevel)
require.Equal(t, "none", *custom.DefaultReasoningLevel)
require.Equal(t, []configuredCodexReasoningLevel{
{Effort: "none", Description: configuredCodexReasoningLevelDescription("none")},
}, 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 TestBuildCodexModelsManifestUsesGPT6AstraInstructions(t *testing.T) {
body, err := BuildCodexModelsManifest([]string{"gpt-6-astra"})
require.NoError(t, err)
var manifest struct {
Models []struct {
ModelMessages struct {
InstructionsTemplate string `json:"instructions_template"`
} `json:"model_messages"`
} `json:"models"`
}
require.NoError(t, json.Unmarshal(body, &manifest))
require.Len(t, manifest.Models, 1)
require.True(t, strings.HasPrefix(
strings.TrimSpace(manifest.Models[0].ModelMessages.InstructionsTemplate),
"You are Codex, an agent based on GPT-6.",
))
}
func effortsFromConfiguredCodexLevels(levels []configuredCodexReasoningLevel) []string {
efforts := make([]string, 0, len(levels))
for _, level := range levels {
efforts = append(efforts, level.Effort)
}
return efforts
}
// Scenario: 无推理模型可直接选中。
func TestBuildCodexModelsManifestUsesSingleNoneReasoningChoiceForCustomModel(t *testing.T) {
t.Parallel()
body, err := BuildCodexModelsManifest([]string{"company-coding-model"})
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, "none", models[0]["default_reasoning_level"])
levels, ok := models[0]["supported_reasoning_levels"].([]any)
require.True(t, ok)
require.Len(t, levels, 1)
firstLevel, ok := levels[0].(map[string]any)
require.True(t, ok)
require.Equal(t, "none", firstLevel["effort"])
}
// Scenario: 已知推理模型保留真实档位。
func TestBuildCodexModelsManifestKeepsKnownReasoningChoices(t *testing.T) {
t.Parallel()
body, err := BuildCodexModelsManifest([]string{"gpt-5.6-sol"})
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, "low", models[0]["default_reasoning_level"])
levels, ok := models[0]["supported_reasoning_levels"].([]any)
require.True(t, ok)
require.Len(t, levels, 6)
firstLevel, ok := levels[0].(map[string]any)
require.True(t, ok)
require.NotEqual(t, "none", firstLevel["effort"])
}
// Scenario: 支持 Fast 的 GPT 型号在目录中声明 priority service tier。
func TestBuildCodexModelsManifestAdvertisesPriorityServiceTierForFastGPTModels(t *testing.T) {
t.Parallel()
body, err := BuildCodexModelsManifest([]string{
"gpt-5.4-mini",
"gpt-5.5",
"gpt-5.6-terra",
})
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 3)
for _, model := range models {
require.Equal(t, []any{
map[string]any{
"id": "priority",
"name": "Fast",
"description": "Priority processing for lower latency.",
},
}, model["service_tiers"])
require.Nil(t, model["default_service_tier"])
}
}
// Scenario: GPT-5.6 Sol 在 Fast 之外额外声明 ultrafast service tier。
func TestBuildCodexModelsManifestAdvertisesUltrafastServiceTierForSol(t *testing.T) {
t.Parallel()
body, err := BuildCodexModelsManifest([]string{
"gpt-5.6-sol",
"gpt-5.6",
})
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 2)
wantTiers := []any{
map[string]any{
"id": "priority",
"name": "Fast",
"description": "Priority processing for lower latency.",
},
map[string]any{
"id": "ultrafast",
"name": "Ultrafast",
"description": "Ultra-low latency processing.",
},
}
for _, model := range models {
require.Equal(t, wantTiers, model["service_tiers"])
require.Nil(t, model["default_service_tier"])
}
}
// Scenario: 未明确支持 Fast 的型号不推测 service tier。
func TestBuildCodexModelsManifestLeavesServiceTiersEmptyForOtherModels(t *testing.T) {
t.Parallel()
body, err := BuildCodexModelsManifest([]string{
"gpt-4o",
"claude-opus-4-6",
"deepseek-v4-pro",
"company-coding-model",
})
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 4)
for _, model := range models {
require.Equal(t, []any{}, model["service_tiers"])
require.Nil(t, model["default_service_tier"])
}
}
// 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"])
require.Equal(t, "Grok 4.5", models[0]["display_name"])
require.Equal(t, []string{"low", "medium", "high"}, effortsFromManifestModel(t, models[0]))
}
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 TestBuildCodexModelsManifestForGroupUsesProviderImageCapabilities(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", "image"},
},
{
name: "custom OpenAI-compatible host with unknown model",
model: "company-coding-model",
accounts: []Account{{
ID: 29, 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 TestBuildCodexModelsManifestForGroupUsesDeepSeekVisionCapabilities(t *testing.T) {
t.Parallel()
const visionModel = "deepseek-v4-flash-vision-exp"
newAccount := func(id int64, platform, model string, modalities []string) Account {
account := Account{
ID: id, Platform: platform, Type: AccountTypeAPIKey,
Credentials: map[string]any{"model_mapping": map[string]any{"vision-alias": model}},
}
if modalities != nil {
account.SetUpstreamModelMetadataSnapshot(UpstreamModelMetadataSnapshot{Models: map[string]UpstreamModelMetadata{
model: {ID: model, InputModalities: modalities},
}})
}
return account
}
tests := []struct {
name string
platform string
accounts []Account
modalities []any
}{
{
name: "native DeepSeek", platform: PlatformDeepseek,
accounts: []Account{newAccount(1, PlatformDeepseek, visionModel, nil)},
modalities: []any{"text", "image"},
},
{
name: "OpenAI-compatible DeepSeek", platform: PlatformOpenAI,
accounts: []Account{newAccount(1, PlatformOpenAI, visionModel, nil)},
modalities: []any{"text", "image"},
},
{
name: "Composite DeepSeek alias", platform: PlatformComposite,
accounts: []Account{newAccount(1, PlatformDeepseek, visionModel, nil)},
modalities: []any{"text", "image"},
},
{
name: "text-only DeepSeek Flash", platform: PlatformDeepseek,
accounts: []Account{newAccount(1, PlatformDeepseek, "deepseek-v4-flash", nil)},
modalities: []any{"text"},
},
{
name: "explicit text-only metadata", platform: PlatformDeepseek,
accounts: []Account{newAccount(1, PlatformDeepseek, visionModel, []string{"text"})},
modalities: []any{"text"},
},
{
name: "mixed vision and text-only alias", platform: PlatformDeepseek,
accounts: []Account{
newAccount(1, PlatformDeepseek, visionModel, nil),
newAccount(2, PlatformDeepseek, "deepseek-v4-flash", nil),
},
modalities: []any{"text"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
const groupID int64 = 790
svc := &GatewayService{accountRepo: codexModelsVisibilityAccountRepo{byGroup: map[int64][]Account{
groupID: tt.accounts,
}}}
body, err := svc.BuildCodexModelsManifestForGroup(context.Background(),
&Group{ID: groupID, Platform: tt.platform}, "", []string{"vision-alias"})
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, tt.modalities, models[0]["input_modalities"])
})
}
}
func TestBuildCodexModelsManifestForGroupPrefersSyncedOpenAIImageCapabilities(t *testing.T) {
t.Parallel()
newAccount := func(id int64, modalities []string) Account {
account := Account{
ID: id, Platform: PlatformOpenAI, Type: AccountTypeAPIKey,
Credentials: map[string]any{"base_url": "https://openai-compatible.example.test/v1"},
}
if modalities != nil {
account.SetUpstreamModelMetadataSnapshot(UpstreamModelMetadataSnapshot{Models: map[string]UpstreamModelMetadata{
"gpt-5.6-sol": {ID: "gpt-5.6-sol", InputModalities: modalities},
}})
}
return account
}
tests := []struct {
name string
accounts []Account
modalities []any
}{
{
name: "explicit text-only snapshot narrows local fallback",
accounts: []Account{
newAccount(30, nil),
newAccount(31, []string{"text"}),
},
modalities: []any{"text"},
},
{
name: "explicit multimodal snapshot preserves local fallback",
accounts: []Account{
newAccount(32, nil),
newAccount(33, []string{"text", "image"}),
},
modalities: []any{"text", "image"},
},
}
for i, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
groupID := int64(760 + i)
svc := &GatewayService{accountRepo: codexModelsVisibilityAccountRepo{byGroup: map[int64][]Account{
groupID: tt.accounts,
}}}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: groupID, Platform: PlatformOpenAI},
"",
[]string{"gpt-5.6-sol"},
)
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"])
}
// Scenario: a Composite exact alias inherits metadata from its unique mapped target model.
func TestBuildCodexModelsManifestForGroupUsesMappedTargetMetadataForCompositeAlias(t *testing.T) {
t.Parallel()
const groupID int64 = 733
svc := &GatewayService{accountRepo: codexModelsVisibilityAccountRepo{byGroup: map[int64][]Account{
groupID: {{
ID: 23,
Platform: PlatformAnthropic,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"model_mapping": map[string]any{"reasoning-alias": "claude-opus-4-8"},
},
}},
}}}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: groupID, Platform: PlatformComposite},
"",
[]string{"reasoning-alias"},
)
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, "reasoning-alias", models[0]["slug"])
require.Equal(t, "reasoning-alias", models[0]["display_name"])
require.Equal(t, "Custom model routed through Sub2API.", models[0]["description"])
require.Equal(t, []string{"low", "medium", "high", "xhigh", "max"}, effortsFromManifestModel(t, models[0]))
}
// Scenario: conflicting targets on the same platform keep the public alias but do not guess capabilities.
func TestBuildCodexModelsManifestForGroupUsesSafeFallbackForConflictingAliasTargets(t *testing.T) {
t.Parallel()
const groupID int64 = 734
svc := &GatewayService{accountRepo: codexModelsVisibilityAccountRepo{byGroup: map[int64][]Account{
groupID: {
{
ID: 24,
Platform: PlatformAnthropic,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"model_mapping": map[string]any{"shared-alias": "claude-opus-4-8"},
},
},
{
ID: 25,
Platform: PlatformAnthropic,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"model_mapping": map[string]any{"shared-alias": "claude-haiku-4-5-20251001"},
},
},
},
}}}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: groupID, Platform: PlatformComposite},
"",
[]string{"shared-alias"},
)
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, "shared-alias", models[0]["slug"])
require.Equal(t, "shared-alias", models[0]["display_name"])
require.Equal(t, "Custom model routed through Sub2API.", models[0]["description"])
require.Empty(t, effortsFromManifestModel(t, models[0]))
}
// Scenario: a media-only target remains hidden even when exposed through an ordinary alias.
func TestBuildCodexModelsManifestForGroupOmitsDedicatedMediaTargetAlias(t *testing.T) {
t.Parallel()
const groupID int64 = 735
svc := &GatewayService{accountRepo: codexModelsVisibilityAccountRepo{byGroup: map[int64][]Account{
groupID: {{
ID: 26,
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"model_mapping": map[string]any{"creative-alias": "gpt-image-2"},
},
}},
}}}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(),
&Group{ID: groupID, Platform: PlatformComposite},
"",
[]string{"creative-alias"},
)
require.NoError(t, err)
require.Empty(t, decodeCodexManifestModels(t, body))
}
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())
require.NotNil(t, repo.groupID)
require.Equal(t, groupID, *repo.groupID)
require.False(t, repo.includeGrouped)
require.Contains(t, repo.platforms, PlatformOpenAI)
require.Contains(t, repo.platforms, PlatformGrok)
require.Contains(t, repo.platforms, PlatformDeepseek)
require.Contains(t, repo.platforms, PlatformMiniMax)
require.Contains(t, repo.platforms, PlatformOpenCodeGo)
require.NotContains(t, repo.platforms, PlatformComposite)
}
func TestBuildCodexModelsManifestForGroupUsesFallbackWhenTextOnlyPlatformHasNoSnapshot(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(1), 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 := &OpenAIModelsResponse{
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)
}
// Mixed groups retain configured metadata alongside defaults for unmapped accounts.
func TestBuildGroupConfiguredCodexModelsManifestUsesAdministratorConfiguration(t *testing.T) {
t.Parallel()
const groupID int64 = 77
reasoning := true
arkAccount := Account{
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"model_mapping": map[string]any{
"glm-5.3": "glm-5.3",
"gpt-image-2": "gpt-image-2",
},
},
}
arkAccount.SetUpstreamModelMetadataSnapshot(UpstreamModelMetadataSnapshot{Models: map[string]UpstreamModelMetadata{
"glm-5.3": {
ID: "glm-5.3",
DisplayName: "GLM 5.3",
Description: "Ark coding model",
Reasoning: &reasoning,
DefaultReasoningLevel: "medium",
SupportedReasoningLevels: []string{"low", "medium", "high"},
InputModalities: []string{"text"},
ContextWindow: 1_000_000,
},
}})
svc := &OpenAIGatewayService{accountRepo: codexModelsVisibilityAccountRepo{
byGroup: map[int64][]Account{
groupID: {
{
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
},
arkAccount,
},
},
}}
group := &Group{ID: groupID, Platform: PlatformOpenAI}
manifest, configured, err := svc.BuildGroupConfiguredCodexModelsManifest(context.Background(), group, "")
require.NoError(t, err)
require.True(t, configured)
models := decodeCodexManifestModels(t, manifest.Body)
require.Equal(t, "glm-5.3", models[0]["slug"])
require.Contains(t, codexManifestModelSlugs(t, manifest.Body), "gpt-5.6-sol")
require.NotContains(t, codexManifestModelSlugs(t, manifest.Body), "gpt-image-2")
require.NotContains(t, codexManifestModelSlugs(t, manifest.Body), "codex-auto-review")
require.Equal(t, "GLM 5.3", models[0]["display_name"])
require.Equal(t, []string{"low", "medium", "high"}, effortsFromManifestModel(t, models[0]))
require.Equal(t, "medium", models[0]["default_reasoning_level"])
require.Equal(t, []any{"text"}, models[0]["input_modalities"])
require.EqualValues(t, 1_000_000, models[0]["context_window"])
require.Equal(t, codexModelsManifestBodyETag(manifest.Body), manifest.ETag)
notModified, configured, err := svc.BuildGroupConfiguredCodexModelsManifest(
context.Background(),
group,
"W/"+manifest.ETag,
)
require.NoError(t, err)
require.True(t, configured)
require.True(t, notModified.NotModified)
require.Empty(t, notModified.Body)
require.Equal(t, manifest.ETag, notModified.ETag)
}
// Scenario: OpenAI 通配映射展开组内精确选择,但不发布通配符 slug。
func TestBuildGroupConfiguredCodexModelsManifestExpandsSelectedModelCoveredByWildcardMapping(t *testing.T) {
t.Parallel()
const groupID int64 = 80
svc := &OpenAIGatewayService{accountRepo: codexModelsVisibilityAccountRepo{
byGroup: map[int64][]Account{
groupID: {{
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"model_mapping": map[string]any{"gpt-*": "gpt-5.6-sol"},
},
}},
},
}}
group := &Group{
ID: groupID,
Platform: PlatformOpenAI,
ModelAllowlist: GroupModelAllowlist{
Enabled: true,
Models: []string{"gpt-5.6"},
},
}
manifest, configured, err := svc.BuildGroupConfiguredCodexModelsManifest(context.Background(), group, "")
require.NoError(t, err)
require.True(t, configured)
require.Equal(t, []string{"gpt-5.6"}, codexManifestModelSlugs(t, manifest.Body))
require.NotContains(t, string(manifest.Body), "gpt-*")
}
// Scenario: OpenAI 配置目录对仅因瞬态状态退出当前调度池的账号取能力交集,
// 且不发布其独有模型。持久 schedulable 仍为 true。
func TestBuildGroupConfiguredCodexModelsManifestIntersectsTransientlyUnschedulableMappedAccounts(t *testing.T) {
t.Parallel()
const groupID int64 = 79
schedulable := newCodexCatalogMappedAccount(
41,
"gpt-5.6-sol",
"GPT-5.6 Sol",
[]string{"low", "medium", "high", "xhigh"},
[]string{"text", "image"},
1_000_000,
true,
nil,
)
transientlyUnschedulable := newCodexCatalogMappedAccount(
42,
"glm-5.3",
"GLM 5.3",
[]string{"low", "medium", "high"},
[]string{"text"},
272_000,
true,
map[string]any{"exclusive-model": "exclusive-upstream"},
)
svc := &OpenAIGatewayService{accountRepo: splitCodexModelsAccountRepo{
schedulable: map[int64][]Account{groupID: {schedulable}},
catalog: map[int64][]Account{groupID: {schedulable, transientlyUnschedulable}},
}}
manifest, configured, err := svc.BuildGroupConfiguredCodexModelsManifest(
context.Background(),
&Group{ID: groupID, Platform: PlatformOpenAI},
"",
)
require.NoError(t, err)
require.True(t, configured)
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 1)
require.Equal(t, "my-coder", models[0]["slug"])
require.Equal(t, "my-coder", models[0]["display_name"])
require.Equal(t, []string{"low", "medium", "high"}, effortsFromManifestModel(t, models[0]))
require.Equal(t, []any{"text"}, models[0]["input_modalities"])
require.EqualValues(t, 272_000, models[0]["context_window"])
}
// Scenario: 管理员持久禁用的账号不能继续收窄 Codex 能力目录。
func TestBuildGroupConfiguredCodexModelsManifestIgnoresPersistentlyDisabledMappedAccounts(t *testing.T) {
t.Parallel()
const groupID int64 = 80
enabled := newCodexCatalogMappedAccount(
51,
"gpt-5.6-sol",
"GPT-5.6 Sol",
[]string{"low", "medium", "high", "xhigh"},
[]string{"text", "image"},
1_000_000,
true,
nil,
)
disabled := newCodexCatalogMappedAccount(
52,
"glm-5.3",
"GLM 5.3",
[]string{"low", "medium", "high"},
[]string{"text"},
272_000,
false,
nil,
)
svc := &OpenAIGatewayService{accountRepo: splitCodexModelsAccountRepo{
schedulable: map[int64][]Account{groupID: {enabled}},
catalog: map[int64][]Account{groupID: {enabled}},
all: map[int64][]Account{groupID: {enabled, disabled}},
}}
manifest, configured, err := svc.BuildGroupConfiguredCodexModelsManifest(
context.Background(),
&Group{ID: groupID, Platform: PlatformOpenAI},
"",
)
require.NoError(t, err)
require.True(t, configured)
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 1)
require.Equal(t, "my-coder", models[0]["slug"])
require.Equal(t, []string{"low", "medium", "high", "xhigh"}, effortsFromManifestModel(t, models[0]))
require.Equal(t, []any{"text", "image"}, models[0]["input_modalities"])
require.EqualValues(t, 1_000_000, models[0]["context_window"])
}
// Scenario: 没有管理员模型配置时保留现有上游发现路径。
func TestBuildGroupConfiguredCodexModelsManifestFallsThroughWithoutConfiguration(t *testing.T) {
t.Parallel()
const groupID int64 = 78
svc := &OpenAIGatewayService{accountRepo: codexModelsVisibilityAccountRepo{
byGroup: map[int64][]Account{
groupID: {{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}},
},
}}
manifest, configured, err := svc.BuildGroupConfiguredCodexModelsManifest(
context.Background(),
&Group{ID: groupID, Platform: PlatformOpenAI},
"",
)
require.NoError(t, err)
require.False(t, configured)
require.Nil(t, manifest)
}
func TestMergeGroupConfiguredCodexModelsFiltersAutoReviewByDefault(t *testing.T) {
t.Parallel()
const groupID int64 = 74
svc := &OpenAIGatewayService{accountRepo: codexModelsVisibilityAccountRepo{}}
manifest := &OpenAIModelsResponse{
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 := &OpenAIModelsResponse{
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 := &OpenAIModelsResponse{
Body: []byte(`{"models":[{"slug":"codex-auto-review","visibility":"list"},{"slug":"gpt-5.6","visibility":"list"}]}`),
}
group := &Group{
ID: groupID,
Platform: PlatformOpenAI,
ModelAllowlist: GroupModelAllowlist{
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,
ModelAllowlist: GroupModelAllowlist{
Enabled: true,
Models: []string{"deepseek-4-pro"},
},
}
upstreamBody := []byte(`{"models":[{"slug":"gpt-5.6","display_name":"GPT-5.6"}]}`)
manifest := &OpenAIModelsResponse{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 := &OpenAIModelsResponse{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{}
startedOnce *sync.Once
release <-chan struct{}
body *strings.Reader
}
func (b *codexModelsBlockingBody) Read(p []byte) (int, error) {
b.startedOnce.Do(func() { close(b.readStarted) })
select {
case <-b.release:
return b.body.Read(p)
case <-b.ctx.Done():
return 0, b.ctx.Err()
}
}
func (b *codexModelsBlockingBody) Close() error { return nil }
func (s *codexModelsHTTPUpstreamStub) Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) {
return s.do(req, proxyURL, accountID, accountConcurrency)
}
func (s *codexModelsHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, _ *tlsfingerprint.Profile) (*http.Response, error) {
return s.Do(req, proxyURL, accountID, accountConcurrency)
}
func TestIsRetryableCodexModelsManifestTransportError(t *testing.T) {
tests := []struct {
name string
err error
retryable bool
}{
{name: "nil", err: nil},
{name: "configuration error", err: errors.New("invalid proxy URL")},
{name: "upstream configuration error", err: errors.New("upstream error: invalid proxy")},
{name: "proxy connection configuration error", err: errors.New("proxy connection error: invalid configuration")},
{name: "canceled request", err: context.Canceled},
{
name: "redirect policy error",
err: &url.Error{
Op: "Get",
URL: "https://upstream.example/v1/models",
Err: errors.New("stopped after 10 redirects"),
},
},
{name: "deadline exceeded", err: context.DeadlineExceeded, retryable: true},
{name: "unexpected EOF", err: io.ErrUnexpectedEOF, retryable: true},
{name: "closed connection", err: net.ErrClosed, retryable: true},
{
name: "network operation",
err: &net.OpError{
Op: "read",
Net: "tcp",
Err: errors.New("connection reset"),
},
retryable: true,
},
{
name: "DNS error",
err: &net.DNSError{Err: "temporary failure", Name: "upstream.example"},
retryable: true,
},
{
name: "typed HTTP2 GOAWAY",
err: http2.GoAwayError{ErrCode: http2.ErrCodeNo},
retryable: true,
},
{
name: "stdlib HTTP2 GOAWAY",
err: errors.New("http2: server sent GOAWAY and closed the connection; LastStreamID=1, ErrCode=NO_ERROR"),
retryable: true,
},
{
name: "stdlib HTTP2 refused stream",
err: errors.New("stream error: stream ID 3; REFUSED_STREAM"),
retryable: true,
},
{
name: "stdlib HTTP2 connection error",
err: errors.New(`Get "https://upstream.example/v1/models": connection error: PROTOCOL_ERROR`),
retryable: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isRetryableCodexModelsManifestTransportError(tt.err); got != tt.retryable {
t.Fatalf("retryable = %v, want %v", got, tt.retryable)
}
})
}
}
func TestIsRetryableCodexModelsManifestStatus(t *testing.T) {
t.Parallel()
tests := []struct {
name string
statusCode int
useAPIKeyUpstream bool
retryable bool
}{
{name: "api key 404", statusCode: http.StatusNotFound, useAPIKeyUpstream: true, retryable: true},
{name: "api key 405", statusCode: http.StatusMethodNotAllowed, useAPIKeyUpstream: true, retryable: true},
{name: "oauth 404", statusCode: http.StatusNotFound},
{name: "oauth 405", statusCode: http.StatusMethodNotAllowed},
{name: "api key 401", statusCode: http.StatusUnauthorized, useAPIKeyUpstream: true},
{name: "oauth 401", statusCode: http.StatusUnauthorized, retryable: true},
{name: "api key 400", statusCode: http.StatusBadRequest, useAPIKeyUpstream: true},
{name: "api key 403", statusCode: http.StatusForbidden, useAPIKeyUpstream: true},
{name: "rate limited", statusCode: http.StatusTooManyRequests, retryable: true},
{name: "server error", statusCode: http.StatusServiceUnavailable, retryable: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.retryable, isRetryableCodexModelsManifestStatus(tt.statusCode, tt.useAPIKeyUpstream))
})
}
}
func newCodexModelsAPIKeyTestService(upstream HTTPUpstream) *OpenAIGatewayService {
return &OpenAIGatewayService{
cfg: &config.Config{Security: config.SecurityConfig{URLAllowlist: config.URLAllowlistConfig{
Enabled: false,
}}},
httpUpstream: upstream,
}
}
func newCodexModelsAPIKeyTestAccount(baseURL string) *Account {
credentials := map[string]any{"api_key": "sk-upstream"}
if baseURL != "" {
credentials["base_url"] = baseURL
}
return &Account{
ID: 2,
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Credentials: credentials,
Concurrency: 3,
}
}
func newCodexModelsTestAccount() *Account {
return &Account{
ID: 1,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Credentials: map[string]any{
"access_token": "test-access-token",
"chatgpt_account_id": "acc-123",
},
}
}
func TestFetchCodexModelsManifestPassthrough(t *testing.T) {
manifestBody := `{"models":[{"slug":"gpt-5.5","display_name":"GPT-5.5"}]}`
var gotAuth, gotAccountID, gotOriginator, gotClientVersion string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
gotAccountID = r.Header.Get("chatgpt-account-id")
gotOriginator = r.Header.Get("Originator")
gotClientVersion = r.URL.Query().Get("client_version")
w.Header().Set("ETag", `W/"abc123"`)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(manifestBody))
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
defer func() { chatgptCodexModelsURL = original }()
s := &OpenAIGatewayService{}
manifest, err := s.FetchCodexModelsManifest(context.Background(), newCodexModelsTestAccount(), "0.137.0", "")
if err != nil {
t.Fatalf("FetchCodexModelsManifest returned error: %v", err)
}
if string(manifest.Body) != manifestBody {
t.Errorf("body not passed through verbatim: got %q", manifest.Body)
}
if manifest.ETag != `W/"abc123"` {
t.Errorf("etag not passed through: got %q", manifest.ETag)
}
if gotAuth != "Bearer test-access-token" {
t.Errorf("authorization header: got %q", gotAuth)
}
if gotAccountID != "acc-123" {
t.Errorf("chatgpt-account-id header: got %q", gotAccountID)
}
if gotOriginator != openai.CodexDefaultOriginator {
t.Errorf("originator header: got %q", gotOriginator)
}
if gotClientVersion != "0.137.0" {
t.Errorf("client_version query: got %q", gotClientVersion)
}
}
func TestFetchCodexModelsManifestAgentIdentityUsesAssertionWithoutOAuthToken(t *testing.T) {
key, privateKey := newTestAgentIdentityKey(t)
account := &Account{
ID: 3,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Credentials: map[string]any{
"auth_mode": OpenAIAuthModeAgentIdentity,
"agent_runtime_id": key.runtimeID,
"agent_private_key": privateKey,
"task_id": key.taskID,
"chatgpt_account_id": "acc-agent",
},
}
var gotAuth, gotAccountID string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
gotAccountID = r.Header.Get("chatgpt-account-id")
_, _ = w.Write([]byte(`{"models":[]}`))
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
defer func() { chatgptCodexModelsURL = original }()
s := &OpenAIGatewayService{}
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
if err != nil {
t.Fatalf("FetchCodexModelsManifest returned error: %v", err)
}
if string(manifest.Body) != `{"models":[]}` {
t.Fatalf("unexpected manifest body: %q", manifest.Body)
}
if !strings.HasPrefix(gotAuth, "AgentAssertion ") {
t.Fatalf("authorization scheme: got %q", strings.SplitN(gotAuth, " ", 2)[0])
}
if gotAccountID != "acc-agent" {
t.Fatalf("chatgpt-account-id header: got %q", gotAccountID)
}
}
func TestFetchCodexModelsManifestAgentIdentityRecoversInvalidTaskOnce(t *testing.T) {
key, privateKey := newTestAgentIdentityKey(t)
account := &Account{
ID: 4,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Credentials: map[string]any{
"auth_mode": OpenAIAuthModeAgentIdentity,
"agent_runtime_id": key.runtimeID,
"agent_private_key": privateKey,
"task_id": "task-models-old",
"chatgpt_account_id": "acc-agent-recovery",
},
}
repo := &stubQuotaAccountRepo{accounts: map[int64]*Account{account.ID: account}}
modelsCalls := 0
registerCalls := 0
var assertions []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
if strings.Contains(r.URL.Path, "/task/register") {
registerCalls++
_, _ = w.Write([]byte(`{"task_id":"task-models-new"}`))
return
}
modelsCalls++
assertions = append(assertions, r.Header.Get("Authorization"))
if modelsCalls == 1 {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":{"code":"invalid_task_id"}}`))
return
}
_, _ = w.Write([]byte(`{"models":[]}`))
}))
defer server.Close()
originalModelsURL := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
t.Cleanup(func() { chatgptCodexModelsURL = originalModelsURL })
originalAuthBase := openAIAgentIdentityAuthAPIBaseURL
openAIAgentIdentityAuthAPIBaseURL = server.URL
t.Cleanup(func() { openAIAgentIdentityAuthAPIBaseURL = originalAuthBase })
s := &OpenAIGatewayService{accountRepo: repo}
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.NoError(t, err)
require.Equal(t, `{"models":[]}`, string(manifest.Body))
require.Equal(t, 2, modelsCalls)
require.Equal(t, 1, registerCalls)
require.Len(t, assertions, 2)
require.Equal(t, "task-models-old", decodeAgentAssertionTask(t, assertions[0]))
require.Equal(t, "task-models-new", decodeAgentAssertionTask(t, assertions[1]))
}
func TestFetchCodexModelsManifestAgentIdentityRedactsUpstreamErrors(t *testing.T) {
key, privateKey := newTestAgentIdentityKey(t)
account := &Account{
ID: 5,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Credentials: map[string]any{
"auth_mode": OpenAIAuthModeAgentIdentity,
"agent_runtime_id": key.runtimeID,
"agent_private_key": privateKey,
"task_id": key.taskID,
"chatgpt_account_id": "acc-agent-redaction",
},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = fmt.Fprintf(w, `{"error":"%s %s %s AgentAssertion leaked"}`, key.runtimeID, key.taskID, privateKey)
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
t.Cleanup(func() { chatgptCodexModelsURL = original })
s := &OpenAIGatewayService{}
_, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.Error(t, err)
require.NotContains(t, err.Error(), key.runtimeID)
require.NotContains(t, err.Error(), key.taskID)
require.NotContains(t, err.Error(), privateKey)
require.NotContains(t, err.Error(), "AgentAssertion leaked")
require.Contains(t, err.Error(), "[redacted]")
}
func TestFetchCodexModelsManifestDefaultClientVersion(t *testing.T) {
var gotClientVersion string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotClientVersion = r.URL.Query().Get("client_version")
_, _ = w.Write([]byte(`{"models":[]}`))
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
defer func() { chatgptCodexModelsURL = original }()
s := &OpenAIGatewayService{}
if _, err := s.FetchCodexModelsManifest(context.Background(), newCodexModelsTestAccount(), "", ""); err != nil {
t.Fatalf("FetchCodexModelsManifest returned error: %v", err)
}
if gotClientVersion != CodexCanonicalClientVersion() {
t.Errorf("default client_version: got %q, want %q", gotClientVersion, CodexCanonicalClientVersion())
}
}
func TestFetchCodexModelsManifestNotModified(t *testing.T) {
manifestBody := `{"models":[{"slug":"gpt-5.5"}]}`
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
calls.Add(1)
w.Header().Set("ETag", `W/"abc123"`)
_, _ = w.Write([]byte(manifestBody))
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
defer func() { chatgptCodexModelsURL = original }()
s := &OpenAIGatewayService{}
account := newCodexModelsTestAccount()
first, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
if err != nil {
t.Fatalf("FetchCodexModelsManifest returned error: %v", err)
}
if first.NotModified {
t.Fatal("cold fetch must return the manifest body")
}
if first.ETag != `W/"abc123"` {
t.Errorf("etag not passed through: got %q", first.ETag)
}
// OAuth 路径接入缓存后,客户端 If-None-Match 与缓存内容 ETag 比较,不再透传上游:
// 新鲜期内命中缓存且匹配直接 304,零上游请求。
second, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", `W/"abc123"`)
if err != nil {
t.Fatalf("cached fetch returned error: %v", err)
}
if !second.NotModified {
t.Fatal("matching cached ETag must return NotModified")
}
if second.ETag != `W/"abc123"` {
t.Errorf("NotModified etag: got %q", second.ETag)
}
if got := calls.Load(); got != 1 {
t.Errorf("upstream calls: got %d, want 1", got)
}
}
func TestFetchCodexModelsManifestUpstreamError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, `{"detail":"boom"}`, http.StatusInternalServerError)
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
defer func() { chatgptCodexModelsURL = original }()
s := &OpenAIGatewayService{}
if _, err := s.FetchCodexModelsManifest(context.Background(), newCodexModelsTestAccount(), "0.137.0", ""); err == nil {
t.Fatal("expected error for upstream 500, got nil")
}
}
func TestFetchCodexModelsManifestMissingToken(t *testing.T) {
account := newCodexModelsTestAccount()
delete(account.Credentials, "access_token")
s := &OpenAIGatewayService{}
if _, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", ""); err == nil {
t.Fatal("expected error for missing access token, got nil")
}
}
func TestFetchCodexModelsManifestAPIKeyCustomUpstream(t *testing.T) {
manifestBody := `{"models":[{"slug":"deepseek-v4-pro"}]}`
var gotRequest *http.Request
var gotProxyURL string
var gotAccountID int64
var gotConcurrency int
upstream := &codexModelsHTTPUpstreamStub{do: func(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) {
gotRequest = req
gotProxyURL = proxyURL
gotAccountID = accountID
gotConcurrency = accountConcurrency
header := make(http.Header)
header.Set("ETag", `W/"api-key-manifest"`)
return &http.Response{
StatusCode: http.StatusOK,
Header: header,
Body: io.NopCloser(strings.NewReader(manifestBody)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
manifest, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount("https://upstream.example/v1"),
"0.144.0",
"",
)
if err != nil {
t.Fatalf("FetchCodexModelsManifest returned error: %v", err)
}
if gotRequest == nil {
t.Fatal("expected request to custom API key upstream")
}
if gotRequest.Method != http.MethodGet {
t.Errorf("method: got %q", gotRequest.Method)
}
if gotRequest.URL.String() != "https://upstream.example/v1/models?client_version=0.144.0" {
t.Errorf("request URL: got %q", gotRequest.URL.String())
}
if gotRequest.Header.Get("Authorization") != "Bearer sk-upstream" {
t.Errorf("authorization header: got %q", gotRequest.Header.Get("Authorization"))
}
if gotRequest.Header.Get("Originator") != openai.CodexDefaultOriginator {
t.Errorf("originator header: got %q", gotRequest.Header.Get("Originator"))
}
if gotRequest.Header.Get("Version") != "0.144.0" {
t.Errorf("version header must match the client_version query param: got %q", gotRequest.Header.Get("Version"))
}
if gotRequest.Header.Get("User-Agent") != CodexCanonicalUserAgent() {
t.Errorf("user-agent header: got %q", gotRequest.Header.Get("User-Agent"))
}
if gotRequest.Header.Get("chatgpt-account-id") != "" {
t.Errorf("chatgpt-account-id must not be sent to API key upstream: got %q", gotRequest.Header.Get("chatgpt-account-id"))
}
if gotProxyURL != "" || gotAccountID != 2 || gotConcurrency != 3 {
t.Errorf("upstream routing metadata: proxy=%q account_id=%d concurrency=%d", gotProxyURL, gotAccountID, gotConcurrency)
}
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)
}
// Scenario: 完整上游清单没有 ETag 时,最终正文仍生成强 ETag 并支持 304。
func TestFetchCodexModelsManifestAPIKeyCompleteBodyWithoutUpstreamETagUsesFinalBodyETag(t *testing.T) {
completeBody, err := BuildCodexModelsManifest([]string{"custom-complete-model"})
require.NoError(t, err)
var calls atomic.Int32
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
calls.Add(1)
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(bytes.NewReader(completeBody)),
}, nil
}}
svc := newCodexModelsAPIKeyTestService(upstream)
svc.accountRepo = codexModelsVisibilityAccountRepo{}
account := newCodexModelsAPIKeyTestAccount("https://upstream.example/v1")
group := &Group{ID: 82, Platform: PlatformOpenAI}
first, err := svc.FetchCodexModelsManifest(context.Background(), account, "0.150.0", "")
require.NoError(t, err)
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(first, account))
require.NoError(t, svc.MergeGroupConfiguredCodexModels(context.Background(), group, first, ""))
require.Equal(t, codexModelsManifestBodyETag(first.Body), first.ETag)
require.NotEmpty(t, first.ETag)
second, err := svc.FetchCodexModelsManifest(context.Background(), account, "0.150.0", "")
require.NoError(t, err)
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(second, account))
require.NoError(t, svc.MergeGroupConfiguredCodexModels(context.Background(), group, second, first.ETag))
require.True(t, second.NotModified)
require.Empty(t, second.Body)
require.Equal(t, int32(1), calls.Load())
}
func TestFetchCodexModelsManifestAPIKeyConvertsStandardOpenAIModelList(t *testing.T) {
upstreamBody := `{"object":"list","data":[{"id":"gpt-5.6","object":"model"},{"id":" ","object":"model"},{"id":"gpt-5.6-codex","object":"model"}]}`
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
header := make(http.Header)
header.Set("ETag", `W/"openai-list"`)
return &http.Response{
StatusCode: http.StatusOK,
Header: header,
Body: io.NopCloser(strings.NewReader(upstreamBody)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
manifest, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount("https://upstream.example/v1"),
"0.144.0",
"",
)
if err != nil {
t.Fatalf("FetchCodexModelsManifest returned error: %v", err)
}
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, []any{"text", "image"}, models[0]["input_modalities"])
require.Equal(t, []any{"text", "image"}, models[1]["input_modalities"])
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 := &OpenAIModelsResponse{
Body: []byte(`{"models":[{"slug":"grok-4.6","description":"Provider supplied","service_tiers":[{"id":"provider-priority","name":"Provider Fast","description":"Provider supplied tier."}],"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{
map[string]any{
"id": "provider-priority",
"name": "Provider Fast",
"description": "Provider supplied tier.",
},
}, models[0]["service_tiers"])
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 TestCompleteAPIKeyCodexModelsManifestForClientUsesKnownGPTImageFallback(t *testing.T) {
t.Parallel()
svc := &OpenAIGatewayService{}
manifest := &OpenAIModelsResponse{Body: []byte(`{"models":[
{"slug":"gpt-5.6-sol"},
{"slug":"company-coding-model"},
{"slug":"gpt-4o","input_modalities":["text"]}
]}`)}
account := newCodexModelsAPIKeyTestAccount("https://openai-compatible.example.test/v1")
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(manifest, account))
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 3)
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
}
require.Equal(t, []any{"text", "image"}, bySlug["gpt-5.6-sol"]["input_modalities"])
require.Equal(t, []any{"text"}, bySlug["company-coding-model"]["input_modalities"])
require.Equal(t, []any{"text"}, bySlug["gpt-4o"]["input_modalities"])
}
// Scenario: 标准 /models 型号列表优先使用已同步账号能力,再使用本地 descriptor 兜底。
func TestCompleteAPIKeyCodexModelsManifestForClientUsesSyncedMetadataForConvertedModelList(t *testing.T) {
t.Parallel()
reasoning := true
account := newCodexModelsAPIKeyTestAccount("https://upstream.example/v1")
account.SetUpstreamModelMetadataSnapshot(UpstreamModelMetadataSnapshot{Models: map[string]UpstreamModelMetadata{
"future-reasoner": {
ID: "future-reasoner", DisplayName: "Future Reasoner", Description: "Synced upstream capability",
Reasoning: &reasoning, DefaultReasoningLevel: "ultra",
SupportedReasoningLevels: []string{"low", "high", "ultra"},
InputModalities: []string{"text", "image"},
ContextWindow: 999_000,
},
}})
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"object":"list","data":[{"id":"future-reasoner","object":"model"}]}`)),
}, nil
}}
svc := newCodexModelsAPIKeyTestService(upstream)
manifest, err := svc.FetchCodexModelsManifest(context.Background(), account, "0.150.0", "")
require.NoError(t, err)
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(manifest, account))
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 1)
require.Equal(t, "Future Reasoner", models[0]["display_name"])
require.Equal(t, "Synced upstream capability", models[0]["description"])
require.Equal(t, "ultra", models[0]["default_reasoning_level"])
require.Equal(t, []string{"low", "high", "ultra"}, effortsFromManifestModel(t, models[0]))
require.Equal(t, []any{"text", "image"}, models[0]["input_modalities"])
require.EqualValues(t, 999_000, models[0]["context_window"])
require.EqualValues(t, 999_000, models[0]["max_context_window"])
}
func TestCompleteAPIKeyCodexModelsManifestForClientFillsMissingProviderFieldsWithoutOverwritingExplicitMetadata(t *testing.T) {
t.Parallel()
reasoning := true
account := newCodexModelsAPIKeyTestAccount("https://upstream.example/v1")
account.SetUpstreamModelMetadataSnapshot(UpstreamModelMetadataSnapshot{Models: map[string]UpstreamModelMetadata{
"provider-model": {
ID: "provider-model", DisplayName: "Synced Display", Description: "Synced description",
Reasoning: &reasoning, DefaultReasoningLevel: "ultra",
SupportedReasoningLevels: []string{"high", "ultra"},
InputModalities: []string{"text", "image"},
ContextWindow: 999_000,
},
}})
manifest := &OpenAIModelsResponse{Body: []byte(`{"models":[{
"slug":"provider-model",
"description":"Provider supplied",
"context_window":64000,
"max_context_window":64000
}]}`)}
svc := &OpenAIGatewayService{}
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(manifest, account))
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 1)
require.Equal(t, "Synced Display", models[0]["display_name"])
require.Equal(t, "Provider supplied", models[0]["description"])
require.Equal(t, "ultra", models[0]["default_reasoning_level"])
require.Equal(t, []string{"high", "ultra"}, effortsFromManifestModel(t, models[0]))
require.Equal(t, []any{"text", "image"}, models[0]["input_modalities"])
require.EqualValues(t, 64_000, models[0]["context_window"])
require.EqualValues(t, 64_000, models[0]["max_context_window"])
}
// Scenario: 原生 manifest 的缺失字段在命中缓存后仍使用账号当前同步快照,而不是缓存中的本地默认值。
func TestCompleteAPIKeyCodexModelsManifestForClientUsesCurrentSnapshotForCachedNativeManifest(t *testing.T) {
t.Parallel()
reasoning := true
account := newCodexModelsAPIKeyTestAccount("https://upstream.example/v1")
setSnapshot := func(displayName string, contextWindow int64) {
account.SetUpstreamModelMetadataSnapshot(UpstreamModelMetadataSnapshot{Models: map[string]UpstreamModelMetadata{
"deepseek-v4-pro": {
ID: "deepseek-v4-pro", DisplayName: displayName,
Reasoning: &reasoning, DefaultReasoningLevel: "ultra",
SupportedReasoningLevels: []string{"high", "ultra"},
InputModalities: []string{"text", "image"},
ContextWindow: contextWindow,
},
}})
}
setSnapshot("Synced DeepSeek", 256_000)
var calls atomic.Int32
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
calls.Add(1)
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"models":[{
"slug":"deepseek-v4-pro",
"description":"Provider supplied"
}]}`)),
}, nil
}}
svc := newCodexModelsAPIKeyTestService(upstream)
first, err := svc.FetchCodexModelsManifest(context.Background(), account, "0.150.0", "")
require.NoError(t, err)
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(first, account))
firstModel := decodeCodexManifestModels(t, first.Body)[0]
require.Equal(t, "Synced DeepSeek", firstModel["display_name"])
require.Equal(t, "Provider supplied", firstModel["description"])
require.Equal(t, "ultra", firstModel["default_reasoning_level"])
require.Equal(t, []string{"high", "ultra"}, effortsFromManifestModel(t, firstModel))
require.Equal(t, []any{"text", "image"}, firstModel["input_modalities"])
require.EqualValues(t, 256_000, firstModel["context_window"])
setSnapshot("Refreshed DeepSeek", 512_000)
second, err := svc.FetchCodexModelsManifest(context.Background(), account, "0.150.0", "")
require.NoError(t, err)
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(second, account))
secondModel := decodeCodexManifestModels(t, second.Body)[0]
require.Equal(t, "Refreshed DeepSeek", secondModel["display_name"])
require.Equal(t, "Provider supplied", secondModel["description"])
require.EqualValues(t, 512_000, secondModel["context_window"])
require.Equal(t, int32(1), calls.Load(), "second response should use the cached upstream source body")
}
func TestCompleteAPIKeyCodexModelsManifestForClientMarksOnlyOfficialVisionGPTImageInput(t *testing.T) {
t.Parallel()
svc := &OpenAIGatewayService{}
manifest := &OpenAIModelsResponse{Body: []byte(`{"models":[{"slug":"gpt-6-astra"},{"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, 5)
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-6-astra", "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 := &OpenAIModelsResponse{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
body string
want string
}{
{
name: "affected models disable responses lite and preserve unknown fields",
body: `{"models":[{"slug":"gpt-6-astra","use_responses_lite":true},{"slug":"gpt-5.6-sol","use_responses_lite":true,"unknown_model":{"enabled":true}},{"slug":"gpt-5.6-terra","use_responses_lite":true},{"slug":"gpt-5.6-luna","use_responses_lite":true}],"unknown_top":{"version":1}}`,
want: `{"models":[{"slug":"gpt-6-astra","use_responses_lite":false},{"slug":"gpt-5.6-sol","unknown_model":{"enabled":true},"use_responses_lite":false},{"slug":"gpt-5.6-terra","use_responses_lite":false},{"slug":"gpt-5.6-luna","use_responses_lite":false}],"unknown_top":{"version":1}}`,
},
{
name: "unaffected model unchanged",
body: ` {"models":[{"slug":"gpt-5.6-codex","use_responses_lite":true}]} `,
want: ` {"models":[{"slug":"gpt-5.6-codex","use_responses_lite":true}]} `,
},
{
name: "false missing and alternate entries unchanged",
body: `{"models":[{"slug":"gpt-5.6-sol","use_responses_lite":false},{"slug":"gpt-5.6-terra"},null,"gpt-5.6-luna",{"slug":17,"use_responses_lite":true}]}`,
want: `{"models":[{"slug":"gpt-5.6-sol","use_responses_lite":false},{"slug":"gpt-5.6-terra"},null,"gpt-5.6-luna",{"slug":17,"use_responses_lite":true}]}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := adjustAPIKeyCodexModelsManifest([]byte(tt.body), nil)
require.NoError(t, err)
require.Equal(t, tt.want, string(got))
})
}
}
func TestFetchCodexModelsManifestAPIKeyDisablesResponsesLiteForAffectedModels(t *testing.T) {
const upstreamBody = `{"models":[{"slug":"gpt-5.6-sol","use_responses_lite":true},{"slug":"gpt-5.6-codex","use_responses_lite":true}],"metadata":{"version":1}}`
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Etag": []string{`"upstream-strong"`}},
Body: io.NopCloser(strings.NewReader(upstreamBody)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
manifest, err := s.FetchCodexModelsManifest(context.Background(), newCodexModelsAPIKeyTestAccount("https://upstream.example"), "0.145.0", "")
require.NoError(t, err)
require.JSONEq(t, `{"models":[{"slug":"gpt-5.6-sol","use_responses_lite":false},{"slug":"gpt-5.6-codex","use_responses_lite":true}],"metadata":{"version":1}}`, string(manifest.Body))
require.Equal(t, codexModelsManifestBodyETag(manifest.Body), manifest.ETag)
require.Equal(t, `"upstream-strong"`, manifest.upstreamETag)
notModified, err := s.FetchCodexModelsManifest(context.Background(), newCodexModelsAPIKeyTestAccount("https://upstream.example"), "0.145.0", manifest.ETag)
require.NoError(t, err)
require.True(t, notModified.NotModified)
require.Equal(t, manifest.ETag, notModified.ETag)
}
func TestFetchCodexModelsManifestOAuthPreservesResponsesLite(t *testing.T) {
const manifestBody = ` {"models":[{"slug":"gpt-5.6-sol","use_responses_lite":true}]} `
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(manifestBody))
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
defer func() { chatgptCodexModelsURL = original }()
s := &OpenAIGatewayService{}
manifest, err := s.FetchCodexModelsManifest(context.Background(), newCodexModelsTestAccount(), "0.145.0", "")
require.NoError(t, err)
require.Equal(t, manifestBody, string(manifest.Body))
}
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: "codex manifest unchanged",
body: `{"models":[{"slug":"m-1"}]}`,
want: `{"models":[{"slug":"m-1"}]}`,
},
{
name: "empty data unchanged",
body: `{"object":"list","data":[]}`,
want: `{"object":"list","data":[]}`,
},
{
name: "data not an array unchanged",
body: `{"object":"list","data":{"id":"m-1"}}`,
want: `{"object":"list","data":{"id":"m-1"}}`,
},
{
name: "entries without usable IDs unchanged",
body: `{"object":"list","data":[{"id":""},{"object":"model"}]}`,
want: `{"object":"list","data":[{"id":""},{"object":"model"}]}`,
},
{
name: "invalid JSON unchanged",
body: `{"data":`,
want: `{"data":`,
},
{
name: "non-object unchanged",
body: `[]`,
want: `[]`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := string(convertOpenAIModelListToCodexManifest([]byte(tt.body))); got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}
func TestFetchCodexModelsManifestUsesConfiguredBodyLimit(t *testing.T) {
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"models":[{"slug":"gpt-5.6"}]}`)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
s.cfg.Gateway.ModelsListReadMaxBytes = 8
_, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount("https://upstream.example"),
"0.144.0",
"",
)
require.Error(t, err)
require.Equal(t, "OPENAI_CODEX_MODELS_UPSTREAM_FAILED", infraerrors.Reason(err))
require.Contains(t, err.Error(), "response exceeds 8 bytes")
require.True(t, IsRetryableCodexModelsManifestError(err))
}
func TestFetchCodexModelsManifestAcceptsConfiguredLimitAboveLegacyBoundary(t *testing.T) {
manifestBody := `{"models":[{"slug":"gpt-5.6","display_name":"` + strings.Repeat("x", (8<<20)+1024) + `"}]}`
require.Greater(t, len(manifestBody), 8<<20)
require.Less(t, len(manifestBody), 16<<20)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, manifestBody)
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
defer func() { chatgptCodexModelsURL = original }()
s := &OpenAIGatewayService{cfg: &config.Config{}}
s.cfg.Gateway.ModelsListReadMaxBytes = 16 << 20
manifest, err := s.FetchCodexModelsManifest(context.Background(), newCodexModelsTestAccount(), "0.144.0", "")
require.NoError(t, err)
require.True(t, bytes.Equal([]byte(manifestBody), manifest.Body), "manifest body must be returned intact")
}
func TestFetchCodexModelsManifestRejectsInvalidEnvelope(t *testing.T) {
tests := []struct {
name string
body string
}{
{name: "OpenAI models list", body: `{"object":"list","data":[]}`},
{name: "invalid JSON", body: `{"models":`},
{name: "non-object", body: `[]`},
{name: "null object", body: `null`},
{name: "missing models", body: `{}`},
{name: "models object", body: `{"models":{}}`},
{name: "models null", body: `{"models":null}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(tt.body)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
_, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount("https://upstream.example"),
"0.144.0",
"",
)
if err == nil {
t.Fatal("expected invalid manifest error, got nil")
}
if infraerrors.Reason(err) != "OPENAI_CODEX_MODELS_UPSTREAM_INVALID_MANIFEST" {
t.Errorf("error reason: got %q", infraerrors.Reason(err))
}
if !IsRetryableCodexModelsManifestError(err) {
t.Error("invalid upstream manifest must be retryable")
}
})
}
}
func TestValidateCodexModelsManifestEnvelopeRequiresExactModelsKey(t *testing.T) {
require.NoError(t, validateCodexModelsManifestEnvelope([]byte(`{"models":[]}`)))
require.Error(t, validateCodexModelsManifestEnvelope([]byte(`{"Models":[]}`)))
}
func TestFetchCodexModelsManifestAPIKeyDoesNotCacheInvalidEnvelope(t *testing.T) {
var calls atomic.Int32
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
body := `{"object":"list","data":[]}`
if calls.Add(1) > 1 {
body = `{"models":[{"slug":"gpt-5.6"}]}`
}
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(body)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
account := newCodexModelsAPIKeyTestAccount("https://upstream.example")
if _, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", ""); err == nil {
t.Fatal("expected invalid manifest error on first fetch")
}
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
if err != nil {
t.Fatalf("second fetch returned error: %v", err)
}
if got, want := string(manifest.Body), `{"models":[{"slug":"gpt-5.6"}]}`; got != want {
t.Errorf("body: got %q, want %q", got, want)
}
if got := calls.Load(); got != 2 {
t.Errorf("upstream calls: got %d, want 2", got)
}
}
func TestFetchCodexModelsManifestAPIKeySharedRefreshSurvivesCallerCancellation(t *testing.T) {
const manifestBody = `{"models":[{"slug":"gpt-5.6"}]}`
var calls atomic.Int32
var readStartedOnce sync.Once
readStarted := make(chan struct{})
deadlineRemaining := make(chan time.Duration, 1)
release := make(chan struct{})
upstream := &codexModelsHTTPUpstreamStub{do: func(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
calls.Add(1)
deadline, ok := req.Context().Deadline()
if !ok {
deadlineRemaining <- 0
} else {
deadlineRemaining <- time.Until(deadline)
}
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Etag": []string{`W/"shared"`}},
Body: &codexModelsBlockingBody{
ctx: req.Context(),
readStarted: readStarted,
startedOnce: &readStartedOnce,
release: release,
body: strings.NewReader(manifestBody),
},
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
account := newCodexModelsAPIKeyTestAccount("https://upstream.example")
firstCtx, cancelFirst := context.WithCancel(context.Background())
firstErr := make(chan error, 1)
go func() {
_, err := s.FetchCodexModelsManifest(firstCtx, account, "0.144.0", "")
firstErr <- err
}()
select {
case <-readStarted:
case <-time.After(time.Second):
t.Fatal("upstream body read did not start")
}
remaining := <-deadlineRemaining
if remaining < 14*time.Second || remaining > codexModelsManifestRequestTimeout {
t.Errorf("detached refresh deadline: got %s, want approximately %s", remaining, codexModelsManifestRequestTimeout)
}
cancelFirst()
select {
case err := <-firstErr:
if !errors.Is(err, context.Canceled) {
t.Fatalf("first caller error: got %v, want context.Canceled", err)
}
case <-time.After(time.Second):
t.Fatal("canceled caller did not return promptly")
}
secondResult := make(chan struct {
manifest *OpenAIModelsResponse
err error
}, 1)
go func() {
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
secondResult <- struct {
manifest *OpenAIModelsResponse
err error
}{manifest: manifest, err: err}
}()
time.Sleep(50 * time.Millisecond)
if got := calls.Load(); got != 1 {
t.Errorf("upstream calls before shared refresh completed: got %d, want 1", got)
}
close(release)
select {
case result := <-secondResult:
if result.err != nil {
t.Fatalf("second caller returned error: %v", result.err)
}
if string(result.manifest.Body) != manifestBody {
t.Errorf("second caller body: got %q", result.manifest.Body)
}
case <-time.After(time.Second):
t.Fatal("second caller did not receive shared refresh result")
}
if got := calls.Load(); got != 1 {
t.Errorf("total upstream calls: got %d, want 1", got)
}
}
func TestFetchCodexModelsManifestAPIKeyConcurrentRequestsShareRefresh(t *testing.T) {
const callers = 8
var calls atomic.Int32
started := make(chan struct{})
var startedOnce sync.Once
release := make(chan struct{})
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
calls.Add(1)
startedOnce.Do(func() { close(started) })
<-release
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"models":[]}`)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
account := newCodexModelsAPIKeyTestAccount("https://upstream.example")
begin := make(chan struct{})
errs := make(chan error, callers)
for i := 0; i < callers; i++ {
go func() {
<-begin
_, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
errs <- err
}()
}
close(begin)
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("upstream request did not start")
}
time.Sleep(50 * time.Millisecond)
if got := calls.Load(); got != 1 {
t.Errorf("concurrent upstream calls: got %d, want 1", got)
}
close(release)
for i := 0; i < callers; i++ {
if err := <-errs; err != nil {
t.Errorf("caller %d returned error: %v", i, err)
}
}
}
func TestFetchCodexModelsManifestAPIKeyFreshCacheHandlesETagLocally(t *testing.T) {
var calls atomic.Int32
upstream := &codexModelsHTTPUpstreamStub{do: func(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
calls.Add(1)
if got := req.Header.Get("If-None-Match"); got != "" {
t.Errorf("cache refresh must not inherit a caller's If-None-Match: got %q", got)
}
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Etag": []string{`W/"cached"`}},
Body: io.NopCloser(strings.NewReader(`{"models":[]}`)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
account := newCodexModelsAPIKeyTestAccount("https://upstream.example")
if _, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", ""); err != nil {
t.Fatalf("initial fetch returned error: %v", err)
}
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", `W/"cached"`)
if err != nil {
t.Fatalf("cached fetch returned error: %v", err)
}
if !manifest.NotModified {
t.Fatal("matching cached ETag must return NotModified")
}
if got := calls.Load(); got != 1 {
t.Errorf("upstream calls: got %d, want 1", got)
}
}
func TestFetchCodexModelsManifestAPIKeyCacheSurvivesClientMutation(t *testing.T) {
var calls atomic.Int32
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
calls.Add(1)
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"object":"list","data":[{"id":"model-a"},{"id":"model-b"}]}`)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
s.accountRepo = codexModelsVisibilityAccountRepo{}
account := newCodexModelsAPIKeyTestAccount("https://upstream.example")
first, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
require.NoError(t, err)
require.Contains(t, string(first.Body), "model-a")
require.Contains(t, string(first.Body), "model-b")
require.NoError(t, s.CompleteAPIKeyCodexModelsManifestForClient(first, account))
require.NoError(t, s.MergeGroupConfiguredCodexModels(
context.Background(),
&Group{
ID: 81,
Platform: PlatformOpenAI,
ModelAllowlist: GroupModelAllowlist{
Enabled: true,
Models: []string{"model-a"},
},
},
first,
"",
))
require.Equal(t, []string{"model-a"}, codexManifestModelSlugs(t, first.Body))
second, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
require.NoError(t, err)
require.Equal(t, []string{"model-a", "model-b"}, codexManifestModelSlugs(t, second.Body))
require.Equal(t, int32(1), calls.Load())
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
manifest, fetchErr := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
require.NoError(t, fetchErr)
require.NoError(t, s.MergeGroupConfiguredCodexModels(
context.Background(),
&Group{
ID: 82,
Platform: PlatformOpenAI,
ModelAllowlist: GroupModelAllowlist{
Enabled: true,
Models: []string{"model-b"},
},
},
manifest,
"",
))
require.Equal(t, []string{"model-b"}, codexManifestModelSlugs(t, manifest.Body))
}()
}
wg.Wait()
third, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
require.NoError(t, err)
require.Equal(t, []string{"model-a", "model-b"}, codexManifestModelSlugs(t, third.Body))
require.Equal(t, int32(1), calls.Load())
}
func TestFetchCodexModelsManifestAPIKeyCacheKeyIsolatesRequestIdentity(t *testing.T) {
var calls atomic.Int32
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
calls.Add(1)
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"models":[]}`)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
base := newCodexModelsAPIKeyTestAccount("https://upstream.example")
fetch := func(account *Account, version string) {
t.Helper()
if _, err := s.FetchCodexModelsManifest(context.Background(), account, version, ""); err != nil {
t.Fatalf("fetch returned error: %v", err)
}
}
fetch(base, "0.144.0")
fetch(base, "0.144.0")
differentAccount := newCodexModelsAPIKeyTestAccount("https://upstream.example")
differentAccount.ID = 3
fetch(differentAccount, "0.144.0")
differentToken := newCodexModelsAPIKeyTestAccount("https://upstream.example")
differentToken.Credentials["api_key"] = "sk-other"
fetch(differentToken, "0.144.0")
differentUpstream := newCodexModelsAPIKeyTestAccount("https://other-upstream.example")
fetch(differentUpstream, "0.144.0")
fetch(base, "0.145.0")
differentHeaders := newCodexModelsAPIKeyTestAccount("https://upstream.example")
differentHeaders.Credentials[credKeyHeaderOverrideEnabled] = true
differentHeaders.Credentials[credKeyHeaderOverrides] = map[string]any{"x-tenant": "other"}
fetch(differentHeaders, "0.144.0")
proxyID := int64(9)
differentProxy := newCodexModelsAPIKeyTestAccount("https://upstream.example")
differentProxy.ProxyID = &proxyID
differentProxy.Proxy = &Proxy{Protocol: "http", Host: "127.0.0.1", Port: 8080}
fetch(differentProxy, "0.144.0")
fetch(differentProxy, "0.144.0")
if got := calls.Load(); got != 7 {
t.Errorf("isolated upstream calls: got %d, want 7", got)
}
}
func TestFetchCodexModelsManifestAPIKeyCacheBoundsEntriesAndBodySize(t *testing.T) {
var calls atomic.Int32
upstream := &codexModelsHTTPUpstreamStub{do: func(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
calls.Add(1)
body := `{"models":[]}`
switch {
case strings.Contains(req.URL.Host, "large-source"):
body = `{"object":"list","data":[{"id":"model-a","padding":"` + strings.Repeat("x", 1<<20) + `"}]}`
case strings.Contains(req.URL.Host, "large"):
body = `{"models":[],"padding":"` + strings.Repeat("x", (1<<20)+1) + `"}`
}
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(body)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
fetch := func(account *Account) {
t.Helper()
if _, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", ""); err != nil {
t.Fatalf("fetch returned error: %v", err)
}
}
small := newCodexModelsAPIKeyTestAccount("https://small.example")
fetch(small)
fetch(small)
large := newCodexModelsAPIKeyTestAccount("https://large.example")
large.ID = 3
fetch(large)
fetch(large)
largeSource := newCodexModelsAPIKeyTestAccount("https://large-source.example")
largeSource.ID = 4
fetch(largeSource)
fetch(largeSource)
if got := calls.Load(); got != 5 {
t.Fatalf("body-size bounded cache calls: got %d, want 5", got)
}
for i := int64(10); i < 523; i++ {
account := newCodexModelsAPIKeyTestAccount("https://bounded.example")
account.ID = i
fetch(account)
}
last := newCodexModelsAPIKeyTestAccount("https://bounded.example")
last.ID = 522
fetch(last)
if got := calls.Load(); got != 518 {
t.Fatalf("most recent cache entry was not retained: calls=%d, want 518", got)
}
first := newCodexModelsAPIKeyTestAccount("https://bounded.example")
first.ID = 10
fetch(first)
if got := calls.Load(); got != 519 {
t.Errorf("oldest cache entry was not evicted: calls=%d, want 519", got)
}
}
func TestFetchCodexModelsManifestAPIKeyServesStaleWhileRefreshing(t *testing.T) {
var calls atomic.Int32
refreshStarted := make(chan struct{})
releaseRefresh := make(chan struct{})
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
call := calls.Add(1)
body := `{"models":[{"slug":"old"}]}`
if call > 1 {
if call == 2 {
close(refreshStarted)
}
<-releaseRefresh
body = `{"models":[{"slug":"new"}]}`
}
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(body)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
account := newCodexModelsAPIKeyTestAccount("https://upstream.example")
if _, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", ""); err != nil {
t.Fatalf("initial fetch returned error: %v", err)
}
s.openAIModelsCache.mu.Lock()
for key, entry := range s.openAIModelsCache.entries {
entry.expiresAt = time.Now().Add(-time.Second)
s.openAIModelsCache.entries[key] = entry
}
s.openAIModelsCache.mu.Unlock()
resultCh := make(chan struct {
manifest *OpenAIModelsResponse
err error
}, 1)
go func() {
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
resultCh <- struct {
manifest *OpenAIModelsResponse
err error
}{manifest: manifest, err: err}
}()
select {
case <-refreshStarted:
case <-time.After(time.Second):
t.Fatal("background refresh did not start")
}
var staleResult struct {
manifest *OpenAIModelsResponse
err error
}
select {
case staleResult = <-resultCh:
case <-time.After(100 * time.Millisecond):
t.Error("stale manifest was not returned while refresh was blocked")
close(releaseRefresh)
staleResult = <-resultCh
}
if staleResult.err != nil {
t.Fatalf("stale fetch returned error: %v", staleResult.err)
}
if got := string(staleResult.manifest.Body); got != `{"models":[{"slug":"old"}]}` {
t.Errorf("stale body: got %q", got)
}
if got := calls.Load(); got != 2 {
t.Errorf("upstream calls during stale refresh: got %d, want 2", got)
}
select {
case <-releaseRefresh:
default:
close(releaseRefresh)
}
deadline := time.Now().Add(time.Second)
for {
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
if err == nil && string(manifest.Body) == `{"models":[{"slug":"new"}]}` {
break
}
if time.Now().After(deadline) {
t.Fatalf("refreshed manifest was not cached: manifest=%v err=%v", manifest, err)
}
time.Sleep(10 * time.Millisecond)
}
if got := calls.Load(); got != 2 {
t.Errorf("stale refresh was not deduplicated: calls=%d, want 2", got)
}
}
func TestFetchCodexModelsManifestAPIKeyRevalidatesStaleETag(t *testing.T) {
var calls atomic.Int32
refreshDone := make(chan struct{})
upstream := &codexModelsHTTPUpstreamStub{do: func(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
call := calls.Add(1)
if call == 1 {
header := make(http.Header)
header.Set("ETag", `"upstream-cached"`)
return &http.Response{
StatusCode: http.StatusOK,
Header: header,
Body: io.NopCloser(strings.NewReader(`{"models":[{"slug":"gpt-5.6-sol","use_responses_lite":true}]}`)),
}, nil
}
if got := req.Header.Get("If-None-Match"); got != `"upstream-cached"` {
t.Errorf("background revalidation If-None-Match: got %q", got)
}
close(refreshDone)
header := make(http.Header)
header.Set("ETag", `"upstream-cached"`)
return &http.Response{StatusCode: http.StatusNotModified, Header: header, Body: http.NoBody}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
account := newCodexModelsAPIKeyTestAccount("https://upstream.example")
if _, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", ""); err != nil {
t.Fatalf("initial fetch returned error: %v", err)
}
s.openAIModelsCache.mu.Lock()
for key, entry := range s.openAIModelsCache.entries {
entry.expiresAt = time.Now().Add(-time.Second)
s.openAIModelsCache.entries[key] = entry
}
s.openAIModelsCache.mu.Unlock()
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
if err != nil {
t.Fatalf("stale fetch returned error: %v", err)
}
if got := string(manifest.Body); got != `{"models":[{"slug":"gpt-5.6-sol","use_responses_lite":false}]}` {
t.Fatalf("stale body: got %q", got)
}
select {
case <-refreshDone:
case <-time.After(time.Second):
t.Fatal("ETag revalidation did not complete")
}
deadline := time.Now().Add(time.Second)
for {
s.openAIModelsCache.mu.Lock()
fresh := false
for _, entry := range s.openAIModelsCache.entries {
fresh = time.Now().Before(entry.expiresAt)
}
s.openAIModelsCache.mu.Unlock()
if fresh {
break
}
if time.Now().After(deadline) {
t.Fatal("304 revalidation did not renew the cached manifest")
}
time.Sleep(10 * time.Millisecond)
}
manifest, err = s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
if err != nil || string(manifest.Body) != `{"models":[{"slug":"gpt-5.6-sol","use_responses_lite":false}]}` {
t.Fatalf("renewed cached manifest: body=%q err=%v", manifest.Body, err)
}
if got := calls.Load(); got != 2 {
t.Errorf("upstream calls: got %d, want 2", got)
}
}
func TestFetchCodexModelsManifestAPIKeyColdCacheHandlesNotModifiedLocally(t *testing.T) {
var gotIfNoneMatch string
upstream := &codexModelsHTTPUpstreamStub{do: func(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
gotIfNoneMatch = req.Header.Get("If-None-Match")
header := make(http.Header)
header.Set("ETag", `W/"api-key-manifest"`)
return &http.Response{
StatusCode: http.StatusOK,
Header: header,
Body: io.NopCloser(strings.NewReader(`{"models":[]}`)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
manifest, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount("https://upstream.example"),
"0.144.0",
`W/"api-key-manifest"`,
)
if err != nil {
t.Fatalf("FetchCodexModelsManifest returned error: %v", err)
}
if !manifest.NotModified {
t.Error("expected NotModified to be true")
}
if manifest.ETag != `W/"api-key-manifest"` {
t.Errorf("etag not passed through: got %q", manifest.ETag)
}
if gotIfNoneMatch != "" {
t.Errorf("cold shared refresh must not inherit caller if-none-match: got %q", gotIfNoneMatch)
}
}
func TestFetchCodexModelsManifestAPIKeyDoesNotCacheUnexpectedColdNotModified(t *testing.T) {
var calls atomic.Int32
upstream := &codexModelsHTTPUpstreamStub{do: func(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
calls.Add(1)
if got := req.Header.Get("If-None-Match"); got != "" {
t.Errorf("cold shared refresh If-None-Match: got %q", got)
}
header := make(http.Header)
header.Set("ETag", `W/"unexpected"`)
return &http.Response{StatusCode: http.StatusNotModified, Header: header, Body: http.NoBody}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
account := newCodexModelsAPIKeyTestAccount("https://upstream.example")
for i := 0; i < 2; i++ {
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.144.0", "")
if err != nil {
t.Fatalf("fetch %d returned error: %v", i, err)
}
if !manifest.NotModified {
t.Fatalf("fetch %d: expected upstream NotModified response", i)
}
}
if got := calls.Load(); got != 2 {
t.Errorf("unexpected cold 304 was cached: upstream calls=%d, want 2", got)
}
}
func TestFetchCodexModelsManifestAPIKeyPreservesBaseURLQuery(t *testing.T) {
var gotURL string
upstream := &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(`{"models":[]}`)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
_, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount("https://upstream.example/v1?tenant=acme"),
"0.144.0",
"",
)
if err != nil {
t.Fatalf("FetchCodexModelsManifest returned error: %v", err)
}
if gotURL != "https://upstream.example/v1/models?client_version=0.144.0&tenant=acme" {
t.Errorf("request URL: got %q", gotURL)
}
}
func TestFetchCodexModelsManifestAPIKeyRejectsBaseURLFragment(t *testing.T) {
called := false
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
called = true
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"models":[]}`)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
_, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount("https://upstream.example/v1#models"),
"0.144.0",
"",
)
if err == nil {
t.Fatal("expected invalid upstream base URL error, got nil")
}
if infraerrors.Reason(err) != "OPENAI_CODEX_MODELS_API_KEY_UPSTREAM_INVALID" {
t.Errorf("error reason: got %q", infraerrors.Reason(err))
}
if called {
t.Fatal("fragment-bearing base URL must be rejected before the upstream request")
}
}
// codexModelsAccountStateRepo records account state transitions triggered by
// manifest upstream errors (#4544).
type codexModelsAccountStateRepo struct {
AccountRepository
mu sync.Mutex
setErrorCalls int
lastErrorMsg string
setTempUnschedCalls int
lastTempReason string
}
func (r *codexModelsAccountStateRepo) SetError(_ context.Context, _ int64, errorMsg string) error {
r.mu.Lock()
defer r.mu.Unlock()
r.setErrorCalls++
r.lastErrorMsg = errorMsg
return nil
}
func (r *codexModelsAccountStateRepo) SetTempUnschedulable(_ context.Context, _ int64, _ time.Time, reason string) error {
r.mu.Lock()
defer r.mu.Unlock()
r.setTempUnschedCalls++
r.lastTempReason = reason
return nil
}
func newCodexModels401TestService(repo AccountRepository) *OpenAIGatewayService {
rateLimitService := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
s := &OpenAIGatewayService{rateLimitService: rateLimitService}
rateLimitService.SetAccountRuntimeBlocker(s)
return s
}
func TestFetchCodexModelsManifestOAuth401MarksAccountUnschedulable(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"detail":{"message":"invalid token"}}`))
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
defer func() { chatgptCodexModelsURL = original }()
repo := &codexModelsAccountStateRepo{}
s := newCodexModels401TestService(repo)
account := newCodexModelsTestAccount()
account.Credentials["refresh_token"] = "test-refresh-token"
_, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.Error(t, err)
require.True(t, IsRetryableCodexModelsManifestError(err), "manifest 401 should allow account failover")
require.Equal(t, 1, repo.setTempUnschedCalls, "OAuth 401 should temp-unschedule the account")
require.Equal(t, 0, repo.setErrorCalls)
require.True(t, s.isOpenAIAccountRuntimeBlocked(account), "account should be runtime-blocked after manifest 401")
}
func TestFetchCodexModelsManifestOAuth401TokenRevokedDisablesAccount(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":{"code":"token_revoked","message":"token has been revoked"}}`))
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
defer func() { chatgptCodexModelsURL = original }()
repo := &codexModelsAccountStateRepo{}
s := newCodexModels401TestService(repo)
account := newCodexModelsTestAccount()
account.Credentials["refresh_token"] = "test-refresh-token"
_, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.Error(t, err)
require.True(t, IsRetryableCodexModelsManifestError(err))
require.Equal(t, 1, repo.setErrorCalls, "revoked token should permanently disable the account")
require.Contains(t, repo.lastErrorMsg, "Token revoked")
require.Equal(t, 0, repo.setTempUnschedCalls)
}
func TestFetchCodexModelsManifestAgentIdentity401DoesNotDisableAccount(t *testing.T) {
key, privateKey := newTestAgentIdentityKey(t)
account := &Account{
ID: 6,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Credentials: map[string]any{
"auth_mode": OpenAIAuthModeAgentIdentity,
"agent_runtime_id": key.runtimeID,
"agent_private_key": privateKey,
"task_id": key.taskID,
"chatgpt_account_id": "acc-agent-401",
},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"detail":"some non-task 401"}`))
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
defer func() { chatgptCodexModelsURL = original }()
repo := &codexModelsAccountStateRepo{}
s := newCodexModels401TestService(repo)
_, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.Error(t, err)
require.Equal(t, 0, repo.setErrorCalls, "agent identity 401s must not disable the account")
require.Equal(t, 0, repo.setTempUnschedCalls)
}
func TestFetchCodexModelsManifestAPIKey401KeepsNoFailoverAndNoDisable(t *testing.T) {
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusUnauthorized,
Status: "401 Unauthorized",
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"error":"invalid api key"}`)),
}, nil
}}
repo := &codexModelsAccountStateRepo{}
s := newCodexModelsAPIKeyTestService(upstream)
s.rateLimitService = NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
_, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount("https://upstream.example"),
"0.144.0",
"",
)
require.Error(t, err)
require.False(t, IsRetryableCodexModelsManifestError(err), "custom upstream manifest 401 keeps the no-failover behavior")
require.Equal(t, 0, repo.setErrorCalls, "custom upstream manifest 401 must not disable the account")
require.Equal(t, 0, repo.setTempUnschedCalls)
}
func TestFetchCodexModelsManifestAPIKeyUpstreamError(t *testing.T) {
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusTooManyRequests,
Status: "429 Too Many Requests",
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"error":"rate limited"}`)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
_, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount("https://upstream.example"),
"0.144.0",
"",
)
if err == nil {
t.Fatal("expected error for upstream 429, got nil")
}
if infraerrors.Code(err) != http.StatusBadGateway {
t.Errorf("error status: got %d, want %d", infraerrors.Code(err), http.StatusBadGateway)
}
if infraerrors.Reason(err) != "OPENAI_CODEX_MODELS_UPSTREAM_FAILED" {
t.Errorf("error reason: got %q", infraerrors.Reason(err))
}
}
func TestFetchCodexModelsManifestAPIKeyUsesOfficialOpenAIModelsEndpoint(t *testing.T) {
tests := []struct {
name string
baseURL string
}{
{name: "missing base URL"},
{name: "official host", baseURL: "https://api.openai.com"},
{name: "official versioned URL", baseURL: "https://API.OPENAI.COM:443/v1/"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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
}})
manifest, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount(tt.baseURL),
"0.144.0",
"",
)
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"])
})
}
}
// --- OAuth manifest 账号级缓存(统一缓存策略) ---
// newCodexModelsOAuthCacheServer 返回一个记录调用次数、返回给定 manifest 的
// ChatGPT Codex models 测试上游。
func newCodexModelsOAuthCacheServer(t *testing.T, body string) (*httptest.Server, *atomic.Int32) {
t.Helper()
calls := &atomic.Int32{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
calls.Add(1)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(body))
}))
t.Cleanup(server.Close)
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
t.Cleanup(func() { chatgptCodexModelsURL = original })
return server, calls
}
func expireCodexModelsManifestCache(s *OpenAIGatewayService, age time.Duration) {
s.openAIModelsCache.mu.Lock()
for key, entry := range s.openAIModelsCache.entries {
entry.expiresAt = time.Now().Add(-age)
entry.staleUntil = time.Now().Add(openAIModelsCacheStaleTTL - age)
s.openAIModelsCache.entries[key] = entry
}
s.openAIModelsCache.mu.Unlock()
}
func TestFetchCodexModelsManifestOAuthFreshWindowZeroUpstreamRequests(t *testing.T) {
_, calls := newCodexModelsOAuthCacheServer(t, `{"models":[{"slug":"gpt-5.5"}]}`)
s := &OpenAIGatewayService{}
account := newCodexModelsTestAccount()
for i := 0; i < 10; i++ {
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.NoError(t, err)
require.False(t, manifest.NotModified)
require.JSONEq(t, `{"models":[{"slug":"gpt-5.5"}]}`, string(manifest.Body))
}
require.EqualValues(t, 1, calls.Load(), "新鲜期内重复请求不得打到 chatgpt.com")
}
func TestFetchCodexModelsManifestOAuthStaleServesOldValueAndRefreshesInBackground(t *testing.T) {
calls := &atomic.Int32{}
refreshStarted := make(chan struct{})
var refreshOnce sync.Once
release := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if calls.Add(1) == 2 {
refreshOnce.Do(func() { close(refreshStarted) })
<-release
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"models":[{"slug":"new"}]}`))
}))
t.Cleanup(server.Close)
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
t.Cleanup(func() { chatgptCodexModelsURL = original })
s := &OpenAIGatewayService{}
account := newCodexModelsTestAccount()
first, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.NoError(t, err)
require.Contains(t, string(first.Body), `"new"`)
expireCodexModelsManifestCache(s, 2*time.Minute)
resultCh := make(chan *OpenAIModelsResponse, 1)
errCh := make(chan error, 1)
go func() {
manifest, fetchErr := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
resultCh <- manifest
errCh <- fetchErr
}()
select {
case <-refreshStarted:
case <-time.After(time.Second):
t.Fatal("乐观期后台刷新未触发")
}
// 乐观期:立即返回旧值,不同步等待刷新。
select {
case manifest := <-resultCh:
require.NoError(t, <-errCh)
require.Contains(t, string(manifest.Body), `"new"`)
case <-time.After(100 * time.Millisecond):
t.Fatal("乐观期必须立即返回缓存旧值")
}
require.EqualValues(t, 2, calls.Load())
close(release)
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
manifest, fetchErr := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
if fetchErr == nil && manifest != nil && !manifest.NotModified {
require.Contains(t, string(manifest.Body), `"new"`)
}
s.openAIModelsCache.mu.Lock()
fresh := len(s.openAIModelsCache.entries) > 0
s.openAIModelsCache.mu.Unlock()
if fresh {
break
}
time.Sleep(10 * time.Millisecond)
}
require.EqualValues(t, 2, calls.Load(), "后台刷新必须单飞")
}
func TestFetchCodexModelsManifestOAuthOverdueSynchronousRefreshAndFailure(t *testing.T) {
t.Run("overdue refresh waits and re-caches", func(t *testing.T) {
calls := &atomic.Int32{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if calls.Add(1) == 2 {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"models":[{"slug":"refreshed"}]}`))
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"models":[{"slug":"stale"}]}`))
}))
t.Cleanup(server.Close)
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
t.Cleanup(func() { chatgptCodexModelsURL = original })
s := &OpenAIGatewayService{}
account := newCodexModelsTestAccount()
_, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.NoError(t, err)
// 超过 5 分钟:缓存条目被丢弃,同步等待上游刷新。
expireCodexModelsManifestCache(s, 6*time.Minute)
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.NoError(t, err)
require.Contains(t, string(manifest.Body), `"refreshed"`)
require.EqualValues(t, 2, calls.Load())
// 新内容写回缓存并重新开始新鲜期。
again, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.NoError(t, err)
require.Contains(t, string(again.Body), `"refreshed"`)
require.EqualValues(t, 2, calls.Load())
})
t.Run("overdue upstream failure returns error not stale cache", func(t *testing.T) {
fail := &atomic.Bool{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if fail.Load() {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"detail":"boom"}`))
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"models":[{"slug":"stale"}]}`))
}))
t.Cleanup(server.Close)
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
t.Cleanup(func() { chatgptCodexModelsURL = original })
s := &OpenAIGatewayService{}
account := newCodexModelsTestAccount()
_, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.NoError(t, err)
fail.Store(true)
expireCodexModelsManifestCache(s, 6*time.Minute)
_, err = s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.Error(t, err, "超期后上游失败必须返回错误而非旧缓存")
require.Equal(t, "OPENAI_CODEX_MODELS_UPSTREAM_FAILED", infraerrors.Reason(err))
})
}
func TestFetchCodexModelsManifestOAuthTokenChangeCacheMiss(t *testing.T) {
_, calls := newCodexModelsOAuthCacheServer(t, `{"models":[]}`)
s := &OpenAIGatewayService{}
account := newCodexModelsTestAccount()
_, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.NoError(t, err)
require.EqualValues(t, 1, calls.Load())
// 令牌刷新后缓存键变化,下一次拉取视为未命中并同步请求上游。
account.Credentials["access_token"] = "rotated-access-token"
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
require.NoError(t, err)
require.Contains(t, string(manifest.Body), `"models"`)
require.EqualValues(t, 2, calls.Load())
}
func TestFetchCodexModelsManifestOAuthSharedAcrossGroupsWithIndependentFiltering(t *testing.T) {
started := make(chan struct{})
var startedOnce sync.Once
release := make(chan struct{})
calls := &atomic.Int32{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
calls.Add(1)
startedOnce.Do(func() { close(started) })
<-release
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"models":[{"slug":"model-a"},{"slug":"model-b"}]}`))
}))
t.Cleanup(server.Close)
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
t.Cleanup(func() { chatgptCodexModelsURL = original })
s := &OpenAIGatewayService{
accountRepo: codexModelsVisibilityAccountRepo{
byGroup: map[int64][]Account{
91: {},
92: {},
},
},
}
account := newCodexModelsTestAccount()
groupA := &Group{ID: 91, Platform: PlatformOpenAI, ModelAllowlist: GroupModelAllowlist{Enabled: true, Models: []string{"model-a"}}}
groupB := &Group{ID: 92, Platform: PlatformOpenAI, ModelAllowlist: GroupModelAllowlist{Enabled: true, Models: []string{"model-b"}}}
begin := make(chan struct{})
type result struct {
slugs []string
err error
}
results := make(chan result, 2)
for _, group := range []*Group{groupA, groupB} {
go func(g *Group) {
<-begin
manifest, err := s.FetchCodexModelsManifest(context.Background(), account, "0.137.0", "")
if err == nil {
err = s.MergeGroupConfiguredCodexModels(context.Background(), g, manifest, "")
}
slugs := []string{}
if err == nil {
slugs = codexManifestModelSlugs(t, manifest.Body)
}
results <- result{slugs: slugs, err: err}
}(group)
}
close(begin)
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("upstream request did not start")
}
close(release)
got := map[int64][]string{91: nil, 92: nil}
for i := 0; i < 2; i++ {
r := <-results
require.NoError(t, r.err)
if len(r.slugs) == 1 && r.slugs[0] == "model-a" {
got[91] = r.slugs
} else if len(r.slugs) == 1 && r.slugs[0] == "model-b" {
got[92] = r.slugs
} else {
t.Fatalf("unexpected filtered slugs: %v", r.slugs)
}
}
require.Equal(t, []string{"model-a"}, got[91])
require.Equal(t, []string{"model-b"}, got[92])
require.EqualValues(t, 1, calls.Load(), "同一账号两个分组同时请求时只发一次上游请求")
}
// Scenario: 非 OpenAI GPT 模型的 Codex 提示词不声称自己是 GPT。
// Antigravity(Google) 对「Codex 提示词 + GPT-5 身份」直接回 429 RESOURCE_EXHAUSTED。
func TestBuildCodexModelsManifestStripsGPTIdentityForNonGPTModels(t *testing.T) {
t.Parallel()
nonGPT := []string{
"gemini-3-flash", "claude-sonnet-4-6", "deepseek-v4-flash", "grok-4.3",
"gpt-oss-120b-medium", "company-coding-model", "company-codex-alias",
}
body, err := BuildCodexModelsManifest(nonGPT)
require.NoError(t, err)
var manifest struct {
Models []struct {
Slug string `json:"slug"`
ModelMessages struct {
InstructionsTemplate string `json:"instructions_template"`
} `json:"model_messages"`
} `json:"models"`
}
require.NoError(t, json.Unmarshal(body, &manifest))
require.Len(t, manifest.Models, len(nonGPT))
for _, m := range manifest.Models {
tmpl := m.ModelMessages.InstructionsTemplate
require.NotContains(t, tmpl, "GPT-", m.Slug)
require.True(t, strings.HasPrefix(tmpl, "You are Codex"), "%s: %.60q", m.Slug, tmpl)
// Only the identity clause is removed; the rest of the bundled prompt is kept.
base := openai.CodexBaseInstructionsForModel(m.Slug)
require.Less(t, len(base)-len(tmpl), 32, m.Slug)
}
}
// Scenario: OpenAI GPT 模型的 Codex 提示词保持原样。
func TestBuildCodexModelsManifestKeepsGPTIdentityForGPTModels(t *testing.T) {
t.Parallel()
for _, model := range []string{"gpt-5.5", "gpt-6-astra", "gpt-5.2", "gpt-5.3-codex-spark"} {
require.Equal(t, openai.CodexBaseInstructionsForModel(model), codexInstructionsTemplateForModel(model), model)
}
}
// Scenario: 每份内置 Codex 提示词的身份声明都能被识别并去掉。
func TestCodexGPTIdentityPatternsCoverBundledPrompts(t *testing.T) {
t.Parallel()
for _, model := range []string{"gpt-5.1", "gpt-5.2", "gpt-5.5", "gpt-6-astra", "gpt-5-codex"} {
tmpl := openai.CodexBaseInstructionsForModel(model)
require.Contains(t, tmpl, "GPT-", "bundled prompt for %s changed; update this test", model)
for _, p := range codexGPTIdentityPatterns {
tmpl = p.re.ReplaceAllString(tmpl, p.repl)
}
require.NotContains(t, tmpl, "GPT-", model)
require.True(t, strings.HasPrefix(tmpl, "You are Codex"), "%s: %.60q", model, tmpl)
}
}
func TestGPT6SolLunaCatalogKeepsAuthoritativeCapabilities(t *testing.T) {
for _, id := range []string{"gpt-6-sol", "gpt-6-luna"} {
svc := &OpenAIGatewayService{}
manifest := &OpenAIModelsResponse{Body: []byte(`{"models":[{"slug":"` + id + `","supported_reasoning_levels":[{"effort":"ultra"}],"default_reasoning_level":"ultra","multi_agent_reasoning_effort":"xhigh","service_tiers":[{"id":"ultrafast"}],"context_window":300000,"max_context_window":900000,"supports_search_tool":false,"apply_patch_tool_type":null}]}`)}
account := newCodexModelsAPIKeyTestAccount("https://api.openai.com/v1")
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(manifest, account))
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 1)
require.Equal(t, []string{"ultra"}, effortsFromManifestModel(t, models[0]))
require.Equal(t, "ultra", models[0]["default_reasoning_level"])
require.Equal(t, "xhigh", models[0]["multi_agent_reasoning_effort"])
require.Equal(t, float64(300000), models[0]["context_window"])
require.Equal(t, float64(900000), models[0]["max_context_window"])
require.Equal(t, []any{map[string]any{"id": "ultrafast"}}, models[0]["service_tiers"])
require.Equal(t, false, models[0]["supports_search_tool"])
require.Contains(t, models[0], "apply_patch_tool_type")
require.Nil(t, models[0]["apply_patch_tool_type"])
}
}