mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-05-23 12:50:10 +08:00
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:
@@ -842,6 +842,15 @@ func (s *Server) watchKeepAlive() {
|
||||
// otherwise it routes to OpenAI handler.
|
||||
func (s *Server) unifiedModelsHandler(openaiHandler *openai.OpenAIAPIHandler, claudeHandler *claude.ClaudeCodeAPIHandler) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if _, ok := c.Request.URL.Query()["client_version"]; ok {
|
||||
if s != nil && s.cfg != nil && s.cfg.Home.Enabled {
|
||||
s.handleHomeCodexClientModels(c)
|
||||
return
|
||||
}
|
||||
openaiHandler.OpenAIModels(c)
|
||||
return
|
||||
}
|
||||
|
||||
if s != nil && s.cfg != nil && s.cfg.Home.Enabled {
|
||||
s.handleHomeModels(c)
|
||||
return
|
||||
@@ -860,6 +869,34 @@ func (s *Server) unifiedModelsHandler(openaiHandler *openai.OpenAIAPIHandler, cl
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleHomeCodexClientModels(c *gin.Context) {
|
||||
entries, ok := s.loadHomeModelEntries(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
models := make([]map[string]any, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
model := map[string]any{
|
||||
"id": entry.id,
|
||||
"object": "model",
|
||||
}
|
||||
if entry.created > 0 {
|
||||
model["created"] = entry.created
|
||||
}
|
||||
if entry.ownedBy != "" {
|
||||
model["owned_by"] = entry.ownedBy
|
||||
}
|
||||
if entry.displayName != "" {
|
||||
model["display_name"] = entry.displayName
|
||||
model["description"] = entry.displayName
|
||||
}
|
||||
models = append(models, model)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, openai.CodexClientModelsResponse(models))
|
||||
}
|
||||
|
||||
func (s *Server) geminiModelsHandler(geminiHandler *gemini.GeminiAPIHandler) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if s != nil && s.cfg != nil && s.cfg.Home.Enabled {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
proxyconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||
internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
||||
sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
||||
sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
|
||||
@@ -239,6 +240,136 @@ func TestAmpProviderModelRoutes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelsWithClientVersionReturnsCodexCatalog(t *testing.T) {
|
||||
modelRegistry := registry.GetGlobalRegistry()
|
||||
clientID := "test-client-version-catalog"
|
||||
modelRegistry.RegisterClient(clientID, "openai", []*registry.ModelInfo{
|
||||
{
|
||||
ID: "gpt-5.5",
|
||||
Object: "model",
|
||||
Created: 1776902400,
|
||||
OwnedBy: "openai",
|
||||
Type: "openai",
|
||||
DisplayName: "GPT 5.5",
|
||||
Description: "Frontier model for complex coding, research, and real-world work.",
|
||||
ContextLength: 272000,
|
||||
Thinking: ®istry.ThinkingSupport{Levels: []string{"low", "medium", "high", "xhigh"}},
|
||||
},
|
||||
{
|
||||
ID: "custom-codex-model-test",
|
||||
Object: "model",
|
||||
OwnedBy: "test",
|
||||
Type: "openai",
|
||||
DisplayName: "Custom Codex Model",
|
||||
Description: "Custom model from registry",
|
||||
ContextLength: 123456,
|
||||
Thinking: ®istry.ThinkingSupport{Levels: []string{"low", "medium"}},
|
||||
},
|
||||
{ID: "grok-imagine-image-quality", Object: "model", OwnedBy: "xai", Type: "openai"},
|
||||
{ID: "gpt-image-2", Object: "model", OwnedBy: "openai", Type: "openai"},
|
||||
{ID: "grok-imagine-image", Object: "model", OwnedBy: "xai", Type: "openai"},
|
||||
{ID: "grok-imagine-video", Object: "model", OwnedBy: "xai", Type: "openai"},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
modelRegistry.UnregisterClient(clientID)
|
||||
})
|
||||
|
||||
server := newTestServer(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/models?client_version", nil)
|
||||
req.Header.Set("Authorization", "Bearer test-key")
|
||||
req.Header.Set("User-Agent", "claude-cli/1.0")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
server.engine.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d body=%s", rr.Code, http.StatusOK, rr.Body.String())
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Models []map[string]any `json:"models"`
|
||||
Object string `json:"object"`
|
||||
Data []any `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to parse response JSON: %v; body=%s", err, rr.Body.String())
|
||||
}
|
||||
if resp.Object != "" || resp.Data != nil {
|
||||
t.Fatalf("expected codex catalog format without object/data, got object=%q data=%v", resp.Object, resp.Data)
|
||||
}
|
||||
if len(resp.Models) == 0 {
|
||||
t.Fatal("expected codex catalog models")
|
||||
}
|
||||
|
||||
var gpt55 map[string]any
|
||||
var custom map[string]any
|
||||
for _, model := range resp.Models {
|
||||
switch slug, _ := model["slug"].(string); slug {
|
||||
case "gpt-5.5":
|
||||
gpt55 = model
|
||||
case "custom-codex-model-test":
|
||||
custom = model
|
||||
}
|
||||
}
|
||||
if gpt55 == nil {
|
||||
t.Fatal("expected gpt-5.5 codex catalog entry")
|
||||
}
|
||||
if _, ok := gpt55["minimal_client_version"]; !ok {
|
||||
t.Fatal("expected minimal_client_version in codex catalog")
|
||||
}
|
||||
serviceTiers, ok := gpt55["service_tiers"].([]any)
|
||||
if !ok || len(serviceTiers) != 1 {
|
||||
t.Fatalf("expected gpt-5.5 priority service tier, got %#v", gpt55["service_tiers"])
|
||||
}
|
||||
if custom == nil {
|
||||
t.Fatal("expected custom model codex catalog entry")
|
||||
}
|
||||
if got, _ := custom["display_name"].(string); got != "Custom Codex Model" {
|
||||
t.Fatalf("custom display_name = %q, want Custom Codex Model", got)
|
||||
}
|
||||
if got, _ := custom["description"].(string); got != "Custom model from registry" {
|
||||
t.Fatalf("custom description = %q, want Custom model from registry", got)
|
||||
}
|
||||
if got, _ := custom["context_window"].(float64); got != 123456 {
|
||||
t.Fatalf("custom context_window = %v, want 123456", custom["context_window"])
|
||||
}
|
||||
if custom["base_instructions"] != gpt55["base_instructions"] {
|
||||
t.Fatal("expected custom model to use gpt-5.5 base_instructions fallback")
|
||||
}
|
||||
if _, ok := custom["available_in_plans"].([]any); !ok {
|
||||
t.Fatalf("expected custom model to use gpt-5.5 available_in_plans fallback, got %#v", custom["available_in_plans"])
|
||||
}
|
||||
if got, _ := custom["prefer_websockets"].(bool); got {
|
||||
t.Fatalf("custom prefer_websockets = %v, want false", custom["prefer_websockets"])
|
||||
}
|
||||
if _, ok := custom["apply_patch_tool_type"]; ok {
|
||||
t.Fatal("expected custom model to omit apply_patch_tool_type")
|
||||
}
|
||||
|
||||
hiddenModels := map[string]bool{
|
||||
"grok-imagine-image-quality": false,
|
||||
"gpt-image-2": false,
|
||||
"grok-imagine-image": false,
|
||||
"grok-imagine-video": false,
|
||||
}
|
||||
for _, model := range resp.Models {
|
||||
slug, _ := model["slug"].(string)
|
||||
if _, ok := hiddenModels[slug]; !ok {
|
||||
continue
|
||||
}
|
||||
if visibility, _ := model["visibility"].(string); visibility != "hide" {
|
||||
t.Fatalf("%s visibility = %q, want hide", slug, visibility)
|
||||
}
|
||||
hiddenModels[slug] = true
|
||||
}
|
||||
for slug, found := range hiddenModels {
|
||||
if !found {
|
||||
t.Fatalf("expected hidden model %s in codex catalog", slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestLoggerFactory_UsesResolvedLogDirectory(t *testing.T) {
|
||||
t.Setenv("WRITABLE_PATH", "")
|
||||
t.Setenv("writable_path", "")
|
||||
|
||||
@@ -31,6 +31,11 @@ var xaiDataTag = []byte("data:")
|
||||
const (
|
||||
xaiImageHandlerType = "openai-image"
|
||||
xaiVideoHandlerType = "openai-video"
|
||||
xaiCustomToolType = "custom"
|
||||
xaiFunctionToolType = "function"
|
||||
xaiImageGenerationToolType = "image_generation"
|
||||
xaiToolSearchType = "tool_search"
|
||||
xaiWebSearchToolType = "web_search"
|
||||
xaiImagesGenerationsPath = "/images/generations"
|
||||
xaiImagesEditsPath = "/images/edits"
|
||||
xaiDefaultImageEndpointPath = xaiImagesGenerationsPath
|
||||
@@ -494,6 +499,7 @@ func (e *XAIExecutor) prepareResponsesRequest(ctx context.Context, req cliproxye
|
||||
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
|
||||
body, _ = sjson.DeleteBytes(body, "safety_identifier")
|
||||
body, _ = sjson.DeleteBytes(body, "stream_options")
|
||||
body = normalizeXAITools(body)
|
||||
body = normalizeCodexInstructions(body)
|
||||
body = sanitizeXAIResponsesBody(body, baseModel)
|
||||
|
||||
@@ -647,6 +653,57 @@ func sanitizeXAIResponsesBody(body []byte, model string) []byte {
|
||||
return body
|
||||
}
|
||||
|
||||
func normalizeXAITools(body []byte) []byte {
|
||||
tools := gjson.GetBytes(body, "tools")
|
||||
if !tools.Exists() || !tools.IsArray() {
|
||||
return body
|
||||
}
|
||||
|
||||
changed := false
|
||||
filtered := []byte(`[]`)
|
||||
for _, tool := range tools.Array() {
|
||||
toolType := tool.Get("type").String()
|
||||
if toolType == xaiToolSearchType || toolType == xaiImageGenerationToolType {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
raw := []byte(tool.Raw)
|
||||
if toolType == xaiCustomToolType {
|
||||
if tool.Get("name").String() == "apply_patch" {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
updatedTool, errSet := sjson.SetBytes(raw, "type", xaiFunctionToolType)
|
||||
if errSet != nil {
|
||||
return body
|
||||
}
|
||||
raw = updatedTool
|
||||
changed = true
|
||||
}
|
||||
if toolType == xaiWebSearchToolType && tool.Get("external_web_access").Exists() {
|
||||
updatedTool, errDel := sjson.DeleteBytes(raw, "external_web_access")
|
||||
if errDel != nil {
|
||||
return body
|
||||
}
|
||||
raw = updatedTool
|
||||
changed = true
|
||||
}
|
||||
updated, errSet := sjson.SetRawBytes(filtered, "-1", raw)
|
||||
if errSet != nil {
|
||||
return body
|
||||
}
|
||||
filtered = updated
|
||||
}
|
||||
if !changed {
|
||||
return body
|
||||
}
|
||||
updated, errSet := sjson.SetRawBytes(body, "tools", filtered)
|
||||
if errSet != nil {
|
||||
return body
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func removeXAIEncryptedReasoningInclude(body []byte) []byte {
|
||||
include := gjson.GetBytes(body, "include")
|
||||
if !include.Exists() || !include.IsArray() {
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestXAIExecutorExecuteShapesResponsesRequest(t *testing.T) {
|
||||
|
||||
_, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
||||
Model: "grok-4.3",
|
||||
Payload: []byte(`{"model":"grok-4.3","input":"hello","include":["reasoning.encrypted_content"],"reasoning":{"effort":"high"}}`),
|
||||
Payload: []byte(`{"model":"grok-4.3","input":"hello","include":["reasoning.encrypted_content"],"reasoning":{"effort":"high"},"tools":[{"type":"tool_search"},{"type":"image_generation"},{"type":"custom","name":"apply_patch"},{"type":"custom","name":"custom_lookup"},{"type":"function","name":"lookup"},{"type":"web_search","external_web_access":true,"search_content_types":["text","image"]}]}`),
|
||||
}, cliproxyexecutor.Options{
|
||||
SourceFormat: sdktranslator.FormatOpenAIResponse,
|
||||
Stream: false,
|
||||
@@ -91,6 +91,30 @@ func TestXAIExecutorExecuteShapesResponsesRequest(t *testing.T) {
|
||||
if gjson.GetBytes(gotBody, "reasoning.effort").String() != "high" {
|
||||
t.Fatalf("reasoning.effort = %q, want high; body=%s", gjson.GetBytes(gotBody, "reasoning.effort").String(), string(gotBody))
|
||||
}
|
||||
tools := gjson.GetBytes(gotBody, "tools").Array()
|
||||
if len(tools) != 3 {
|
||||
t.Fatalf("tools length = %d, want 3; body=%s", len(tools), string(gotBody))
|
||||
}
|
||||
for i, tool := range tools {
|
||||
toolType := tool.Get("type").String()
|
||||
if toolType == "image_generation" {
|
||||
t.Fatalf("tools.%d.type = image_generation, want removed; body=%s", i, string(gotBody))
|
||||
}
|
||||
if toolType != "function" && toolType != "web_search" {
|
||||
t.Fatalf("tools.%d.type = %q, want function or web_search; body=%s", i, toolType, string(gotBody))
|
||||
}
|
||||
if got := tool.Get("name").String(); got == "apply_patch" {
|
||||
t.Fatalf("tools.%d.name = apply_patch, want removed; body=%s", i, string(gotBody))
|
||||
}
|
||||
if toolType == "web_search" {
|
||||
if tool.Get("external_web_access").Exists() {
|
||||
t.Fatalf("tools.%d.external_web_access exists, want removed; body=%s", i, string(gotBody))
|
||||
}
|
||||
if got := tool.Get("search_content_types.1").String(); got != "image" {
|
||||
t.Fatalf("tools.%d.search_content_types missing image entry; body=%s", i, string(gotBody))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, include := range gjson.GetBytes(gotBody, "include").Array() {
|
||||
if include.String() == "reasoning.encrypted_content" {
|
||||
t.Fatalf("xai request must not ask for encrypted reasoning content: %s", string(gotBody))
|
||||
@@ -137,6 +161,68 @@ func TestXAIExecutorOmitsUnsupportedReasoningEffort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestXAIExecutorExecuteStreamFiltersToolSearchTool(t *testing.T) {
|
||||
var gotBody []byte
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var errRead error
|
||||
gotBody, errRead = io.ReadAll(r.Body)
|
||||
if errRead != nil {
|
||||
t.Fatalf("read body: %v", errRead)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
_, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"created_at\":0,\"status\":\"completed\",\"model\":\"grok-4.3\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"ok\"}]}]}}\n\n"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
exec := NewXAIExecutor(&config.Config{})
|
||||
auth := &cliproxyauth.Auth{
|
||||
Provider: "xai",
|
||||
Attributes: map[string]string{"base_url": server.URL},
|
||||
Metadata: map[string]any{"access_token": "xai-token"},
|
||||
}
|
||||
|
||||
result, err := exec.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
|
||||
Model: "grok-4.3",
|
||||
Payload: []byte(`{"model":"grok-4.3","input":"hello","tools":[{"type":"tool_search"},{"type":"image_generation"},{"type":"custom","name":"apply_patch"},{"type":"custom","name":"custom_lookup"},{"type":"function","name":"lookup"},{"type":"web_search","external_web_access":true,"search_content_types":["text","image"]}]}`),
|
||||
}, cliproxyexecutor.Options{
|
||||
SourceFormat: sdktranslator.FormatOpenAIResponse,
|
||||
Stream: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteStream() error = %v", err)
|
||||
}
|
||||
for chunk := range result.Chunks {
|
||||
if chunk.Err != nil {
|
||||
t.Fatalf("stream chunk error = %v", chunk.Err)
|
||||
}
|
||||
}
|
||||
|
||||
tools := gjson.GetBytes(gotBody, "tools").Array()
|
||||
if len(tools) != 3 {
|
||||
t.Fatalf("tools length = %d, want 3; body=%s", len(tools), string(gotBody))
|
||||
}
|
||||
for i, tool := range tools {
|
||||
toolType := tool.Get("type").String()
|
||||
if toolType == "image_generation" {
|
||||
t.Fatalf("tools.%d.type = image_generation, want removed; body=%s", i, string(gotBody))
|
||||
}
|
||||
if toolType != "function" && toolType != "web_search" {
|
||||
t.Fatalf("tools.%d.type = %q, want function or web_search; body=%s", i, toolType, string(gotBody))
|
||||
}
|
||||
if got := tool.Get("name").String(); got == "apply_patch" {
|
||||
t.Fatalf("tools.%d.name = apply_patch, want removed; body=%s", i, string(gotBody))
|
||||
}
|
||||
if toolType == "web_search" {
|
||||
if tool.Get("external_web_access").Exists() {
|
||||
t.Fatalf("tools.%d.external_web_access exists, want removed; body=%s", i, string(gotBody))
|
||||
}
|
||||
if got := tool.Get("search_content_types.1").String(); got != "image" {
|
||||
t.Fatalf("tools.%d.search_content_types missing image entry; body=%s", i, string(gotBody))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXAIExecutorExecuteImagesUsesImagesEndpoint(t *testing.T) {
|
||||
var gotPath string
|
||||
var gotAuth string
|
||||
|
||||
255
sdk/api/handlers/openai/codex_client_models.go
Normal file
255
sdk/api/handlers/openai/codex_client_models.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
516
sdk/api/handlers/openai/codex_client_models.json
Normal file
516
sdk/api/handlers/openai/codex_client_models.json
Normal file
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