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.
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -8,6 +8,7 @@ package openai
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -29,6 +30,9 @@ type OpenAIAPIHandler struct {
|
||||
*handlers.BaseAPIHandler
|
||||
}
|
||||
|
||||
//go:embed codex_client_models.json
|
||||
var codexClientModelsJSON []byte
|
||||
|
||||
// NewOpenAIAPIHandler creates a new OpenAI API handlers instance.
|
||||
// It takes an BaseAPIHandler instance as input and returns an OpenAIAPIHandler.
|
||||
//
|
||||
@@ -59,6 +63,11 @@ func (h *OpenAIAPIHandler) Models() []map[string]any {
|
||||
// It returns a list of available AI models with their capabilities
|
||||
// and specifications in OpenAI-compatible format.
|
||||
func (h *OpenAIAPIHandler) OpenAIModels(c *gin.Context) {
|
||||
if _, ok := c.Request.URL.Query()["client_version"]; ok {
|
||||
c.JSON(http.StatusOK, h.codexClientModelsResponse())
|
||||
return
|
||||
}
|
||||
|
||||
// Get all available models
|
||||
allModels := h.Models()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user