Files
CLIProxyAPI/sdk/api/handlers/openai/codex_client_models.go
T
Luis Pater 088ab33df8 feat(api): add Codex client models support for OpenAI API
- Introduced Codex client models framework in `openai` package.
- Added JSON-based model definitions (`codex_client_models.json`) for Codex, including metadata, reasoning levels, and configuration options.
- Implemented handlers to load, clone, and build Codex client models with support for visibility overrides and metadata application.
- Enabled sorting and prioritization of models based on configuration or runtime criteria.
- Added utility functions for managing and validating model attributes.
2026-05-17 04:48:34 +08:00

256 lines
6.3 KiB
Go

package openai
import (
"encoding/json"
"sort"
"strings"
"sync"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
)
type codexClientModelsPayload struct {
Models []map[string]any `json:"models"`
}
var (
codexClientModelTemplatesOnce sync.Once
codexClientModelTemplates map[string]map[string]any
codexClientDefaultTemplate map[string]any
codexClientModelTemplatesErr error
)
func (h *OpenAIAPIHandler) codexClientModelsResponse() map[string]any {
return CodexClientModelsResponse(h.Models())
}
func CodexClientModelsResponse(models []map[string]any) map[string]any {
return map[string]any{
"models": buildCodexClientModels(models),
}
}
func buildCodexClientModels(models []map[string]any) []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)
applyCodexClientVisibilityOverride(entry, id)
result = append(result, entry)
continue
}
entry := cloneCodexClientModelMap(defaultTemplate)
applyCodexClientModelMetadata(entry, id, model)
applyCodexClientVisibilityOverride(entry, id)
result = append(result, entry)
}
sort.SliceStable(result, func(i, j int) bool {
return codexClientModelPriority(result[i]) < codexClientModelPriority(result[j])
})
return result
}
func loadCodexClientModelTemplates() (map[string]map[string]any, map[string]any, error) {
codexClientModelTemplatesOnce.Do(func() {
var payload codexClientModelsPayload
codexClientModelTemplatesErr = json.Unmarshal(codexClientModelsJSON, &payload)
if codexClientModelTemplatesErr != nil {
return
}
codexClientModelTemplates = make(map[string]map[string]any, len(payload.Models))
for _, model := range payload.Models {
slug := strings.TrimSpace(stringModelValue(model, "slug"))
if slug == "" {
continue
}
codexClientModelTemplates[slug] = cloneCodexClientModelMap(model)
if slug == "gpt-5.5" {
codexClientDefaultTemplate = cloneCodexClientModelMap(model)
}
}
})
return codexClientModelTemplates, codexClientDefaultTemplate, codexClientModelTemplatesErr
}
func applyCodexClientModelMetadata(entry map[string]any, id string, model map[string]any) {
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
}
applyCodexClientThinkingMetadata(entry, info.Thinking)
}
if displayName == "" {
displayName = id
}
if description == "" {
description = id
}
entry["slug"] = id
entry["display_name"] = displayName
entry["description"] = description
entry["priority"] = 100
entry["prefer_websockets"] = false
delete(entry, "apply_patch_tool_type")
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-2", "grok-imagine-image", "grok-imagine-video":
entry["visibility"] = "hide"
}
}
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 := ""
for _, rawLevel := range thinking.Levels {
level := strings.ToLower(strings.TrimSpace(rawLevel))
if level == "" || level == "none" {
continue
}
if defaultLevel == "" || level == "medium" {
defaultLevel = level
}
levels = append(levels, map[string]any{
"effort": level,
"description": codexClientReasoningDescription(level),
})
}
if len(levels) == 0 {
return
}
entry["supported_reasoning_levels"] = levels
entry["default_reasoning_level"] = defaultLevel
}
func codexClientReasoningDescription(level string) string {
switch level {
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"
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
}
}