package models import ( "testing" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" ) func TestCodexClientModelsResponse_InputModalitiesFromRegistry(t *testing.T) { modelID := "mimo-v2.5-pro-codex-test" textOnlyModelID := "mimo-text-only-codex-test" modelRegistry := registry.GetGlobalRegistry() modelRegistry.RegisterClient("codex-input-modalities-test", "openai-compatibility", []*registry.ModelInfo{ { ID: modelID, Object: "model", OwnedBy: "mimo", Type: "openai-compatibility", DisplayName: modelID, SupportedInputModalities: []string{"text", "image"}, }, { ID: textOnlyModelID, Object: "model", OwnedBy: "mimo", Type: "openai-compatibility", DisplayName: textOnlyModelID, SupportedInputModalities: []string{"text"}, }, { ID: "mimo-mixed-modalities-codex-test", Object: "model", OwnedBy: "mimo", Type: "openai-compatibility", DisplayName: "mimo-mixed-modalities-codex-test", SupportedInputModalities: []string{"text", "image", "audio", "video", "TEXT", "IMAGE"}, }, { ID: "compat-image-only-codex-test", Object: "model", OwnedBy: "mimo", Type: registry.OpenAIImageModelType, }, }) t.Cleanup(func() { modelRegistry.UnregisterClient("codex-input-modalities-test") }) openaiModels := modelRegistry.GetAvailableModels("openai") resp := BuildResponse(openaiModels, nil, false) models, ok := resp["models"].([]map[string]any) if !ok { t.Fatalf("models type = %T, want []map[string]any", resp["models"]) } var visionEntry map[string]any var textOnlyEntry map[string]any var mixedEntry map[string]any var imageEntry map[string]any for _, entry := range models { slug := stringModelValue(entry, "slug") switch slug { case modelID: visionEntry = entry case textOnlyModelID: textOnlyEntry = entry case "mimo-mixed-modalities-codex-test": mixedEntry = entry case "compat-image-only-codex-test": imageEntry = entry } } if visionEntry == nil { t.Fatalf("expected codex entry for %q", modelID) } modalities, ok := visionEntry["input_modalities"].([]any) if !ok || len(modalities) != 2 { t.Fatalf("input_modalities = %#v, want [text image]", visionEntry["input_modalities"]) } if got, _ := modalities[0].(string); got != "text" { t.Fatalf("input_modalities[0] = %q, want text", got) } if got, _ := modalities[1].(string); got != "image" { t.Fatalf("input_modalities[1] = %q, want image", got) } if got, ok := visionEntry["supports_image_detail_original"].(bool); !ok || !got { t.Fatalf("supports_image_detail_original = %#v, want true", visionEntry["supports_image_detail_original"]) } if textOnlyEntry == nil { t.Fatalf("expected codex entry for %q", textOnlyModelID) } textOnlyModalities, ok := textOnlyEntry["input_modalities"].([]any) if !ok || len(textOnlyModalities) != 1 { t.Fatalf("text-only input_modalities = %#v, want [text]", textOnlyEntry["input_modalities"]) } if got, _ := textOnlyModalities[0].(string); got != "text" { t.Fatalf("text-only input_modalities[0] = %q, want text", got) } if _, exists := textOnlyEntry["supports_image_detail_original"]; exists { t.Fatalf("text-only model should not expose supports_image_detail_original: %#v", textOnlyEntry["supports_image_detail_original"]) } if mixedEntry == nil { t.Fatal("expected codex entry for mixed-modalities model") } mixedModalities, ok := mixedEntry["input_modalities"].([]any) if !ok || len(mixedModalities) != 2 { t.Fatalf("mixed input_modalities = %#v, want [text image]", mixedEntry["input_modalities"]) } if got, _ := mixedModalities[0].(string); got != "text" { t.Fatalf("mixed input_modalities[0] = %q, want text", got) } if got, _ := mixedModalities[1].(string); got != "image" { t.Fatalf("mixed input_modalities[1] = %q, want image", got) } if got, ok := mixedEntry["supports_image_detail_original"].(bool); !ok || !got { t.Fatalf("mixed supports_image_detail_original = %#v, want true", mixedEntry["supports_image_detail_original"]) } if imageEntry == nil { t.Fatal("expected codex entry for image-only compat model") } if got, _ := imageEntry["visibility"].(string); got != "hide" { t.Fatalf("image model visibility = %q, want hide", got) } if _, exists := imageEntry["input_modalities"]; exists { t.Fatalf("image endpoint model should not expose input_modalities from registry: %#v", imageEntry["input_modalities"]) } } func TestCodexClientModelsResponse_AppliesDisplayNameToTemplateModel(t *testing.T) { resp := BuildResponse([]map[string]any{{ "id": "gpt-5.5", "display_name": "Configured Codex Name", }}, nil, false) models, ok := resp["models"].([]map[string]any) if !ok || len(models) != 1 { t.Fatalf("models = %#v, want one model", resp["models"]) } if got := stringModelValue(models[0], "display_name"); got != "Configured Codex Name" { t.Fatalf("display_name = %q, want Configured Codex Name", got) } } func TestCodexClientModelsResponse_RewritesTemplateMultiAgentVersionWhenEnabled(t *testing.T) { modelIDs := []string{"gpt-5.6-luna", "gpt-5.5"} resp := BuildResponse([]map[string]any{{"id": modelIDs[0]}, {"id": modelIDs[1]}}, nil, true) models, ok := resp["models"].([]map[string]any) if !ok { t.Fatalf("models type = %T, want []map[string]any", resp["models"]) } for _, model := range models { if got := stringModelValue(model, "multi_agent_version"); got != "v2" { t.Errorf("%s multi_agent_version = %q, want v2", stringModelValue(model, "slug"), got) } } } func TestCodexClientModelsResponse_DisablesSearchToolForSynthesizedModels(t *testing.T) { resp := BuildResponse([]map[string]any{ {"id": "custom-openai-compatible-model"}, {"id": "gpt-5.5"}, }, nil, false) models, ok := resp["models"].([]map[string]any) if !ok { t.Fatalf("models type = %T, want []map[string]any", resp["models"]) } bySlug := make(map[string]map[string]any, len(models)) for _, model := range models { bySlug[stringModelValue(model, "slug")] = model } custom := bySlug["custom-openai-compatible-model"] if custom == nil { t.Fatal("expected synthesized custom model entry") } if got, ok := custom["supports_search_tool"].(bool); !ok || got { t.Fatalf("custom supports_search_tool = %#v, want false", custom["supports_search_tool"]) } official := bySlug["gpt-5.5"] if official == nil { t.Fatal("expected official template model entry") } if got, ok := official["supports_search_tool"].(bool); !ok || !got { t.Fatalf("official supports_search_tool = %#v, want true", official["supports_search_tool"]) } } func TestCodexClientModelsResponse_RequiresTemplateAndCodexProvidersForSearchTool(t *testing.T) { providers := map[string][]string{ "new-codex-model": {"codex"}, "gpt-5.5": {"openai-compatible-deepseek"}, "gpt-5.4": {"codex", "xai"}, "gpt-5.6-sol": {"codex"}, } resp := BuildResponse([]map[string]any{ {"id": "new-codex-model"}, {"id": "gpt-5.5"}, {"id": "gpt-5.4"}, {"id": "gpt-5.6-sol"}, }, func(id string) []string { return providers[id] }, false) models, ok := resp["models"].([]map[string]any) if !ok { t.Fatalf("models type = %T, want []map[string]any", resp["models"]) } bySlug := make(map[string]map[string]any, len(models)) for _, model := range models { bySlug[stringModelValue(model, "slug")] = model } if got, ok := bySlug["gpt-5.6-sol"]["supports_search_tool"].(bool); !ok || !got { t.Errorf("gpt-5.6-sol supports_search_tool = %#v, want true", bySlug["gpt-5.6-sol"]["supports_search_tool"]) } for _, slug := range []string{"new-codex-model", "gpt-5.5", "gpt-5.4"} { if got, ok := bySlug[slug]["supports_search_tool"].(bool); !ok || got { t.Errorf("%s supports_search_tool = %#v, want false", slug, bySlug[slug]["supports_search_tool"]) } } } func TestCodexClientModelsResponse_PreservesUltraReasoningEffort(t *testing.T) { resp := BuildResponse([]map[string]any{{"id": "gpt-5.6-sol"}}, nil, false) models, ok := resp["models"].([]map[string]any) if !ok { t.Fatalf("models type = %T, want []map[string]any", resp["models"]) } var sol map[string]any for _, entry := range models { if stringModelValue(entry, "slug") == "gpt-5.6-sol" { sol = entry break } } if sol == nil { t.Fatal("expected codex client entry for gpt-5.6-sol") } levels, ok := sol["supported_reasoning_levels"].([]any) if !ok { t.Fatalf("supported_reasoning_levels = %T, want []any", sol["supported_reasoning_levels"]) } for _, rawLevel := range levels { level, ok := rawLevel.(map[string]any) if ok && stringModelValue(level, "effort") == "ultra" { return } } t.Fatalf("supported_reasoning_levels = %#v, want ultra", levels) } func TestLoadCodexClientModelTemplatesRefreshesOnRevision(t *testing.T) { codexClientModelTemplatesMu.Lock() previousLoaded := codexClientModelTemplatesLoaded previousRevision := codexClientModelTemplatesRevision previousTemplates := codexClientModelTemplates previousDefault := codexClientDefaultTemplate previousErr := codexClientModelTemplatesErr codexClientModelTemplatesLoaded = false codexClientModelTemplatesMu.Unlock() t.Cleanup(func() { codexClientModelTemplatesMu.Lock() codexClientModelTemplatesLoaded = previousLoaded codexClientModelTemplatesRevision = previousRevision codexClientModelTemplates = previousTemplates codexClientDefaultTemplate = previousDefault codexClientModelTemplatesErr = previousErr codexClientModelTemplatesMu.Unlock() }) first := []byte(`{"models":[{"slug":"gpt-5.5","display_name":"First"}]}`) templates, defaultTemplate, err := loadCodexClientModelTemplatesSnapshot(first, 100) if err != nil { t.Fatalf("load first snapshot: %v", err) } if got := stringModelValue(templates["gpt-5.5"], "display_name"); got != "First" { t.Fatalf("first display_name = %q, want First", got) } if got := stringModelValue(defaultTemplate, "display_name"); got != "First" { t.Fatalf("first default display_name = %q, want First", got) } second := []byte(`{"models":[{"slug":"gpt-5.5","display_name":"Second"}]}`) templates, defaultTemplate, err = loadCodexClientModelTemplatesSnapshot(second, 101) if err != nil { t.Fatalf("load second snapshot: %v", err) } if got := stringModelValue(templates["gpt-5.5"], "display_name"); got != "Second" { t.Fatalf("second display_name = %q, want Second", got) } if got := stringModelValue(defaultTemplate, "display_name"); got != "Second" { t.Fatalf("second default display_name = %q, want Second", got) } templates, _, err = loadCodexClientModelTemplatesSnapshot(first, 101) if err != nil { t.Fatalf("reload cached revision: %v", err) } if got := stringModelValue(templates["gpt-5.5"], "display_name"); got != "Second" { t.Fatalf("cached display_name = %q, want Second", got) } } func TestApplyCodexClientModelMetadataPreservesMultiAgentVersionWhenDisabled(t *testing.T) { entry := map[string]any{"multi_agent_version": "v1"} model := map[string]any{"id": "custom-model"} applyCodexClientModelMetadata(entry, "custom-model", model, false) if got := entry["multi_agent_version"]; got != "v1" { t.Fatalf("disabled multi_agent_version = %#v, want preserved v1", got) } applyCodexClientModelMetadata(entry, "custom-model", model, true) if got := entry["multi_agent_version"]; got != "v2" { t.Fatalf("enabled multi_agent_version = %#v, want v2", got) } } func TestCodexClientModelsResponseAppliesMaxContextLengthOverride(t *testing.T) { const wantOverride = 1048576 const wantDefault = 272000 resp := BuildResponse([]map[string]any{ {"id": "deepseek-v4-flash", "max_context_length": wantOverride}, {"id": "deepseek-v4-pro"}, {"id": "gpt-5.5", "max_context_length": wantOverride}, }, nil, false) models, ok := resp["models"].([]map[string]any) if !ok { t.Fatalf("models type = %T, want []map[string]any", resp["models"]) } bySlug := make(map[string]map[string]any, len(models)) for _, model := range models { bySlug[stringModelValue(model, "slug")] = model } for _, testCase := range []struct { slug string want int }{ {slug: "deepseek-v4-flash", want: wantOverride}, {slug: "deepseek-v4-pro", want: wantDefault}, {slug: "gpt-5.5", want: wantOverride}, } { entry := bySlug[testCase.slug] if entry == nil { t.Fatalf("missing model %q", testCase.slug) } if got := intModelValue(entry, "context_window"); got != testCase.want { t.Errorf("%s context_window = %d, want %d", testCase.slug, got, testCase.want) } if got := intModelValue(entry, "max_context_window"); got != testCase.want { t.Errorf("%s max_context_window = %d, want %d", testCase.slug, got, testCase.want) } } } func TestCodexClientModelsResponseMapsMaxCompletionTokensToMaxTokens(t *testing.T) { const wantTemplateLimit = 64000 const wantSynthesizedLimit = 32000 resp := BuildResponse([]map[string]any{ {"id": "gpt-5.5", "max_completion_tokens": wantTemplateLimit}, {"id": "custom-output-limit-model", "max_completion_tokens": wantSynthesizedLimit}, }, nil, false) models, ok := resp["models"].([]map[string]any) if !ok { t.Fatalf("models type = %T, want []map[string]any", resp["models"]) } bySlug := make(map[string]map[string]any, len(models)) for _, model := range models { bySlug[stringModelValue(model, "slug")] = model } for _, testCase := range []struct { slug string want int }{ {slug: "gpt-5.5", want: wantTemplateLimit}, {slug: "custom-output-limit-model", want: wantSynthesizedLimit}, } { entry := bySlug[testCase.slug] if entry == nil { t.Fatalf("missing model %q", testCase.slug) } if got := intModelValue(entry, "max_tokens"); got != testCase.want { t.Errorf("%s max_tokens = %d, want %d", testCase.slug, got, testCase.want) } } }