diff --git a/internal/api/server_routes.go b/internal/api/server_routes.go index d57af9c70..6102b83b4 100644 --- a/internal/api/server_routes.go +++ b/internal/api/server_routes.go @@ -675,6 +675,9 @@ func (s *Server) handleHomeCodexClientModels(c *gin.Context) { model["display_name"] = entry.displayName model["description"] = entry.displayName } + if entry.maxCompletionTokens > 0 { + model["max_completion_tokens"] = entry.maxCompletionTokens + } models = append(models, model) } diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 21f78281c..30df27914 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -1832,15 +1832,16 @@ func TestModelsWithClientVersionReturnsCodexCatalog(t *testing.T) { 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: "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, + MaxCompletionTokens: 64000, + Thinking: ®istry.ThinkingSupport{Levels: []string{"low", "medium", "high", "xhigh"}}, }, { ID: "custom-codex-model-test", @@ -1908,6 +1909,9 @@ func TestModelsWithClientVersionReturnsCodexCatalog(t *testing.T) { if _, ok := gpt55["minimal_client_version"]; !ok { t.Fatal("expected minimal_client_version in codex catalog") } + if got, _ := gpt55["max_tokens"].(float64); got != 64000 { + t.Fatalf("gpt-5.5 max_tokens = %v, want 64000", gpt55["max_tokens"]) + } 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"]) diff --git a/internal/client/codex/models/models.go b/internal/client/codex/models/models.go index 3dd684f57..1c9e5794c 100644 --- a/internal/client/codex/models/models.go +++ b/internal/client/codex/models/models.go @@ -61,6 +61,7 @@ func buildCodexClientModels(models []map[string]any, providersForModel Providers entry := cloneCodexClientModelMap(template) applyCodexClientDisplayName(entry, model) applyCodexClientMaxContextLengthOverride(entry, model) + applyCodexClientMaxTokens(entry, model) applyCodexClientSearchToolSupport(entry, id, true, providersForModel) sanitizeCodexClientReasoningMetadata(entry) applyCodexClientVisibilityOverride(entry, id) @@ -73,6 +74,7 @@ func buildCodexClientModels(models []map[string]any, providersForModel Providers entry := cloneCodexClientModelMap(defaultTemplate) applyCodexClientModelMetadata(entry, id, model, optimizeMultiAgentV2) + applyCodexClientMaxTokens(entry, model) applyCodexClientSearchToolSupport(entry, id, false, providersForModel) sanitizeCodexClientReasoningMetadata(entry) applyCodexClientVisibilityOverride(entry, id) @@ -193,6 +195,12 @@ func applyCodexClientMaxContextLengthOverride(entry map[string]any, model map[st } } +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 { diff --git a/internal/client/codex/models/models_test.go b/internal/client/codex/models/models_test.go index f009ca7cf..6500a8f7a 100644 --- a/internal/client/codex/models/models_test.go +++ b/internal/client/codex/models/models_test.go @@ -363,3 +363,38 @@ func TestCodexClientModelsResponseAppliesMaxContextLengthOverride(t *testing.T) } } } + +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) + } + } +}