Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
502
backend/internal/client/codex/models/models.go
Normal file
502
backend/internal/client/codex/models/models.go
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
// Package models builds model catalogs for official Codex clients.
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
||||
)
|
||||
|
||||
type codexClientModelsPayload struct {
|
||||
Models []map[string]any `json:"models"`
|
||||
}
|
||||
|
||||
// ProvidersForModelFunc returns the providers registered for a model.
|
||||
type ProvidersForModelFunc func(string) []string
|
||||
|
||||
var (
|
||||
codexClientModelTemplatesMu sync.Mutex
|
||||
codexClientModelTemplatesLoaded bool
|
||||
codexClientModelTemplatesRevision uint64
|
||||
codexClientModelTemplates map[string]map[string]any
|
||||
codexClientDefaultTemplate map[string]any
|
||||
codexClientModelTemplatesErr error
|
||||
)
|
||||
|
||||
var codexClientAllowedReasoningLevels = map[string]struct{}{
|
||||
"none": {},
|
||||
"minimal": {},
|
||||
"low": {},
|
||||
"medium": {},
|
||||
"high": {},
|
||||
"xhigh": {},
|
||||
"max": {},
|
||||
"ultra": {},
|
||||
}
|
||||
|
||||
// BuildResponse builds a Codex client model response from available models.
|
||||
func BuildResponse(availableModels []map[string]any, providersForModel ProvidersForModelFunc, optimizeMultiAgentV2 bool) map[string]any {
|
||||
return map[string]any{
|
||||
"models": buildCodexClientModels(availableModels, providersForModel, optimizeMultiAgentV2),
|
||||
}
|
||||
}
|
||||
|
||||
func buildCodexClientModels(models []map[string]any, providersForModel ProvidersForModelFunc, optimizeMultiAgentV2 bool) []map[string]any {
|
||||
templates, defaultTemplate, err := loadCodexClientModelTemplates()
|
||||
if err != nil || defaultTemplate == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]map[string]any, 0, len(models))
|
||||
for _, model := range models {
|
||||
id := strings.TrimSpace(stringModelValue(model, "id"))
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if template, ok := templates[id]; ok {
|
||||
entry := cloneCodexClientModelMap(template)
|
||||
applyCodexClientDisplayName(entry, model)
|
||||
applyCodexClientMaxContextLengthOverride(entry, model)
|
||||
applyCodexClientMaxTokens(entry, model)
|
||||
applyCodexClientSearchToolSupport(entry, id, true, providersForModel)
|
||||
sanitizeCodexClientReasoningMetadata(entry)
|
||||
applyCodexClientVisibilityOverride(entry, id)
|
||||
if optimizeMultiAgentV2 {
|
||||
entry["multi_agent_version"] = "v2"
|
||||
}
|
||||
result = append(result, entry)
|
||||
continue
|
||||
}
|
||||
|
||||
entry := cloneCodexClientModelMap(defaultTemplate)
|
||||
applyCodexClientModelMetadata(entry, id, model, optimizeMultiAgentV2)
|
||||
applyCodexClientMaxTokens(entry, model)
|
||||
applyCodexClientSearchToolSupport(entry, id, false, providersForModel)
|
||||
sanitizeCodexClientReasoningMetadata(entry)
|
||||
applyCodexClientVisibilityOverride(entry, id)
|
||||
result = append(result, entry)
|
||||
}
|
||||
|
||||
applyCodexClientNonTemplatePriorities(result, templates)
|
||||
|
||||
sort.SliceStable(result, func(i, j int) bool {
|
||||
return codexClientModelPriority(result[i]) < codexClientModelPriority(result[j])
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func maxCodexClientTemplatePriority(templates map[string]map[string]any) int {
|
||||
maxPriority := 0
|
||||
for _, template := range templates {
|
||||
priority := codexClientModelPriority(template)
|
||||
if priority > maxPriority {
|
||||
maxPriority = priority
|
||||
}
|
||||
}
|
||||
return maxPriority
|
||||
}
|
||||
|
||||
func applyCodexClientNonTemplatePriorities(result []map[string]any, templates map[string]map[string]any) {
|
||||
if len(result) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
basePriority := maxCodexClientTemplatePriority(templates)
|
||||
type nonTemplateEntry struct {
|
||||
index int
|
||||
displayName string
|
||||
slug string
|
||||
}
|
||||
|
||||
pending := make([]nonTemplateEntry, 0)
|
||||
for index, entry := range result {
|
||||
slug := stringModelValue(entry, "slug")
|
||||
if _, ok := templates[slug]; ok {
|
||||
continue
|
||||
}
|
||||
displayName := stringModelValue(entry, "display_name")
|
||||
if displayName == "" {
|
||||
displayName = slug
|
||||
}
|
||||
pending = append(pending, nonTemplateEntry{
|
||||
index: index,
|
||||
displayName: displayName,
|
||||
slug: slug,
|
||||
})
|
||||
}
|
||||
|
||||
sort.SliceStable(pending, func(i, j int) bool {
|
||||
left := strings.ToLower(pending[i].displayName)
|
||||
right := strings.ToLower(pending[j].displayName)
|
||||
if left == right {
|
||||
return pending[i].slug < pending[j].slug
|
||||
}
|
||||
return left < right
|
||||
})
|
||||
|
||||
for rank, entry := range pending {
|
||||
result[entry.index]["priority"] = basePriority + 100*(rank+1)
|
||||
}
|
||||
}
|
||||
|
||||
func loadCodexClientModelTemplates() (map[string]map[string]any, map[string]any, error) {
|
||||
raw, revision := registry.GetCodexClientModelsSnapshot()
|
||||
return loadCodexClientModelTemplatesSnapshot(raw, revision)
|
||||
}
|
||||
|
||||
func loadCodexClientModelTemplatesSnapshot(raw []byte, revision uint64) (map[string]map[string]any, map[string]any, error) {
|
||||
codexClientModelTemplatesMu.Lock()
|
||||
defer codexClientModelTemplatesMu.Unlock()
|
||||
if codexClientModelTemplatesLoaded && codexClientModelTemplatesRevision == revision {
|
||||
return codexClientModelTemplates, codexClientDefaultTemplate, codexClientModelTemplatesErr
|
||||
}
|
||||
|
||||
var payload codexClientModelsPayload
|
||||
err := json.Unmarshal(raw, &payload)
|
||||
var templates map[string]map[string]any
|
||||
var defaultTemplate map[string]any
|
||||
if err == nil {
|
||||
templates = make(map[string]map[string]any, len(payload.Models))
|
||||
for _, model := range payload.Models {
|
||||
slug := strings.TrimSpace(stringModelValue(model, "slug"))
|
||||
if slug == "" {
|
||||
continue
|
||||
}
|
||||
templates[slug] = cloneCodexClientModelMap(model)
|
||||
if slug == "gpt-5.5" {
|
||||
defaultTemplate = cloneCodexClientModelMap(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
codexClientModelTemplatesLoaded = true
|
||||
codexClientModelTemplatesRevision = revision
|
||||
codexClientModelTemplates = templates
|
||||
codexClientDefaultTemplate = defaultTemplate
|
||||
codexClientModelTemplatesErr = err
|
||||
return codexClientModelTemplates, codexClientDefaultTemplate, codexClientModelTemplatesErr
|
||||
}
|
||||
|
||||
func applyCodexClientDisplayName(entry map[string]any, model map[string]any) {
|
||||
if displayName := stringModelValue(model, "display_name"); displayName != "" {
|
||||
entry["display_name"] = displayName
|
||||
}
|
||||
}
|
||||
|
||||
func applyCodexClientMaxContextLengthOverride(entry map[string]any, model map[string]any) {
|
||||
if maxContextLength := intModelValue(model, "max_context_length"); maxContextLength > 0 {
|
||||
entry["context_window"] = maxContextLength
|
||||
entry["max_context_window"] = maxContextLength
|
||||
}
|
||||
}
|
||||
|
||||
func applyCodexClientMaxTokens(entry map[string]any, model map[string]any) {
|
||||
if maxCompletionTokens := intModelValue(model, "max_completion_tokens"); maxCompletionTokens > 0 {
|
||||
entry["max_tokens"] = maxCompletionTokens
|
||||
}
|
||||
}
|
||||
|
||||
func applyCodexClientSearchToolSupport(entry map[string]any, id string, templateModel bool, providersForModel ProvidersForModelFunc) {
|
||||
supportsSearch, _ := entry["supports_search_tool"].(bool)
|
||||
if !supportsSearch {
|
||||
return
|
||||
}
|
||||
|
||||
if !templateModel {
|
||||
entry["supports_search_tool"] = false
|
||||
return
|
||||
}
|
||||
|
||||
if providersForModel == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providers := providersForModel(id)
|
||||
if len(providers) == 0 {
|
||||
entry["supports_search_tool"] = false
|
||||
return
|
||||
}
|
||||
for _, provider := range providers {
|
||||
if !strings.EqualFold(strings.TrimSpace(provider), "codex") {
|
||||
entry["supports_search_tool"] = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyCodexClientModelMetadata(entry map[string]any, id string, model map[string]any, optimizeMultiAgentV2 bool) {
|
||||
info := registry.LookupModelInfo(id)
|
||||
|
||||
displayName := stringModelValue(model, "display_name")
|
||||
description := stringModelValue(model, "description")
|
||||
contextWindow := intModelValue(model, "context_length")
|
||||
|
||||
if info != nil {
|
||||
if info.DisplayName != "" {
|
||||
displayName = info.DisplayName
|
||||
}
|
||||
if info.Description != "" {
|
||||
description = info.Description
|
||||
}
|
||||
if info.ContextLength > 0 {
|
||||
contextWindow = info.ContextLength
|
||||
}
|
||||
if info.Type == registry.OpenAIImageModelType {
|
||||
entry["visibility"] = "hide"
|
||||
delete(entry, "input_modalities")
|
||||
delete(entry, "supports_image_detail_original")
|
||||
} else {
|
||||
applyCodexClientInputModalitiesMetadata(entry, info.SupportedInputModalities)
|
||||
}
|
||||
applyCodexClientThinkingMetadata(entry, info.Thinking)
|
||||
}
|
||||
|
||||
if maxContextWindow := intModelValue(model, "max_context_length"); maxContextWindow > 0 {
|
||||
contextWindow = maxContextWindow
|
||||
}
|
||||
|
||||
if displayName == "" {
|
||||
displayName = id
|
||||
}
|
||||
if description == "" {
|
||||
description = id
|
||||
}
|
||||
|
||||
entry["slug"] = id
|
||||
entry["display_name"] = displayName
|
||||
entry["description"] = description
|
||||
entry["prefer_websockets"] = false
|
||||
if optimizeMultiAgentV2 {
|
||||
entry["multi_agent_version"] = "v2"
|
||||
}
|
||||
entry["service_tiers"] = []any{}
|
||||
delete(entry, "apply_patch_tool_type")
|
||||
delete(entry, "upgrade")
|
||||
delete(entry, "availability_nux")
|
||||
|
||||
if contextWindow > 0 {
|
||||
entry["context_window"] = contextWindow
|
||||
entry["max_context_window"] = contextWindow
|
||||
}
|
||||
|
||||
if baseInstructions := stringModelValue(model, "base_instructions"); baseInstructions != "" {
|
||||
entry["base_instructions"] = baseInstructions
|
||||
}
|
||||
if plans, ok := model["available_in_plans"]; ok {
|
||||
entry["available_in_plans"] = cloneCodexClientModelValue(plans)
|
||||
}
|
||||
}
|
||||
|
||||
func applyCodexClientVisibilityOverride(entry map[string]any, id string) {
|
||||
switch strings.TrimSpace(id) {
|
||||
case "grok-imagine-image-quality", "gpt-image-1.5", "gpt-image-2", "grok-imagine-image", "grok-imagine-image-2.0", "grok-imagine-video", "grok-imagine-video-1.5", "grok-imagine-video-1.5-preview":
|
||||
entry["visibility"] = "hide"
|
||||
}
|
||||
}
|
||||
|
||||
func applyCodexClientInputModalitiesMetadata(entry map[string]any, modalities []string) {
|
||||
if len(modalities) == 0 {
|
||||
return
|
||||
}
|
||||
// Codex client only accepts text/image input modalities.
|
||||
codexModalities := make([]any, 0, 2)
|
||||
seen := make(map[string]struct{}, 2)
|
||||
supportsImage := false
|
||||
for _, raw := range modalities {
|
||||
switch modality := strings.ToLower(strings.TrimSpace(raw)); modality {
|
||||
case "text", "image":
|
||||
if _, ok := seen[modality]; ok {
|
||||
continue
|
||||
}
|
||||
seen[modality] = struct{}{}
|
||||
codexModalities = append(codexModalities, modality)
|
||||
if modality == "image" {
|
||||
supportsImage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(codexModalities) == 0 {
|
||||
return
|
||||
}
|
||||
entry["input_modalities"] = codexModalities
|
||||
if supportsImage {
|
||||
entry["supports_image_detail_original"] = true
|
||||
} else {
|
||||
delete(entry, "supports_image_detail_original")
|
||||
}
|
||||
}
|
||||
|
||||
func applyCodexClientThinkingMetadata(entry map[string]any, thinking *registry.ThinkingSupport) {
|
||||
if thinking == nil || len(thinking.Levels) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
levels := make([]any, 0, len(thinking.Levels))
|
||||
defaultLevel := ""
|
||||
firstLevel := ""
|
||||
for _, rawLevel := range thinking.Levels {
|
||||
level := normalizeCodexClientReasoningLevel(rawLevel)
|
||||
if level == "" {
|
||||
continue
|
||||
}
|
||||
if firstLevel == "" {
|
||||
firstLevel = level
|
||||
}
|
||||
if (defaultLevel == "" && level != "none") || level == "medium" {
|
||||
defaultLevel = level
|
||||
}
|
||||
levels = append(levels, map[string]any{
|
||||
"effort": level,
|
||||
"description": codexClientReasoningDescription(level),
|
||||
})
|
||||
}
|
||||
if len(levels) == 0 {
|
||||
return
|
||||
}
|
||||
if defaultLevel == "" {
|
||||
defaultLevel = firstLevel
|
||||
}
|
||||
|
||||
entry["supported_reasoning_levels"] = levels
|
||||
entry["default_reasoning_level"] = defaultLevel
|
||||
}
|
||||
|
||||
func sanitizeCodexClientReasoningMetadata(entry map[string]any) {
|
||||
rawLevels, ok := entry["supported_reasoning_levels"].([]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
levels := make([]any, 0, len(rawLevels))
|
||||
allowedDefaults := make(map[string]struct{}, len(rawLevels))
|
||||
for _, rawLevelEntry := range rawLevels {
|
||||
levelEntry, ok := rawLevelEntry.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
level := normalizeCodexClientReasoningLevel(stringModelValue(levelEntry, "effort"))
|
||||
if level == "" {
|
||||
continue
|
||||
}
|
||||
clonedEntry := cloneCodexClientModelMap(levelEntry)
|
||||
clonedEntry["effort"] = level
|
||||
levels = append(levels, clonedEntry)
|
||||
allowedDefaults[level] = struct{}{}
|
||||
}
|
||||
|
||||
if len(levels) == 0 {
|
||||
delete(entry, "supported_reasoning_levels")
|
||||
delete(entry, "default_reasoning_level")
|
||||
return
|
||||
}
|
||||
|
||||
defaultLevel := normalizeCodexClientReasoningLevel(stringModelValue(entry, "default_reasoning_level"))
|
||||
if _, ok := allowedDefaults[defaultLevel]; !ok {
|
||||
defaultLevel = stringModelValue(levels[0].(map[string]any), "effort")
|
||||
}
|
||||
|
||||
entry["supported_reasoning_levels"] = levels
|
||||
entry["default_reasoning_level"] = defaultLevel
|
||||
}
|
||||
|
||||
func normalizeCodexClientReasoningLevel(rawLevel string) string {
|
||||
level := strings.ToLower(strings.TrimSpace(rawLevel))
|
||||
if _, ok := codexClientAllowedReasoningLevels[level]; !ok {
|
||||
return ""
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
func codexClientReasoningDescription(level string) string {
|
||||
switch level {
|
||||
case "none":
|
||||
return "No reasoning"
|
||||
case "minimal":
|
||||
return "Fastest responses with minimal reasoning"
|
||||
case "low":
|
||||
return "Fast responses with lighter reasoning"
|
||||
case "medium":
|
||||
return "Balances speed and reasoning depth for everyday tasks"
|
||||
case "high":
|
||||
return "Greater reasoning depth for complex problems"
|
||||
case "xhigh":
|
||||
return "Extra high reasoning depth for complex problems"
|
||||
case "max":
|
||||
return "Maximum available reasoning depth for complex problems"
|
||||
default:
|
||||
return level
|
||||
}
|
||||
}
|
||||
|
||||
func codexClientModelPriority(model map[string]any) int {
|
||||
if priority, ok := model["priority"].(int); ok {
|
||||
return priority
|
||||
}
|
||||
if priority, ok := model["priority"].(float64); ok {
|
||||
return int(priority)
|
||||
}
|
||||
return 100
|
||||
}
|
||||
|
||||
func stringModelValue(model map[string]any, key string) string {
|
||||
if model == nil {
|
||||
return ""
|
||||
}
|
||||
value, ok := model[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if s, ok := value.(string); ok {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func intModelValue(model map[string]any, key string) int {
|
||||
if model == nil {
|
||||
return 0
|
||||
}
|
||||
switch value := model[key].(type) {
|
||||
case int:
|
||||
return value
|
||||
case int64:
|
||||
return int(value)
|
||||
case float64:
|
||||
return int(value)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func cloneCodexClientModelMap(model map[string]any) map[string]any {
|
||||
if model == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[string]any, len(model))
|
||||
for key, value := range model {
|
||||
cloned[key] = cloneCodexClientModelValue(value)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneCodexClientModelValue(value any) any {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
return cloneCodexClientModelMap(typed)
|
||||
case []any:
|
||||
cloned := make([]any, len(typed))
|
||||
for i, entry := range typed {
|
||||
cloned[i] = cloneCodexClientModelValue(entry)
|
||||
}
|
||||
return cloned
|
||||
case []string:
|
||||
return append([]string(nil), typed...)
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
400
backend/internal/client/codex/models/models_test.go
Normal file
400
backend/internal/client/codex/models/models_test.go
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue