From 8e5a12057ab5733cb3c71e05aed8c29c5f295740 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Mon, 7 Sep 2026 14:55:43 -0700 Subject: [PATCH 1/2] feat(ui): list the ChatGPT subscription provider in the Add Model form The Add Model provider dropdown is driven entirely by provider_create_fields.json, and chatgpt had no entry there, so the documented ChatGPT subscription setup was unreachable from the Admin UI. Add the entry plus the dashboard enum, slug, logo and placeholder mappings so the provider can be selected and its cost-map models listed. The entry carries no credential fields on purpose: the chatgpt backend ignores api_key and api_base and signs in through the device-code auth file on the proxy host, so any field here would be inert. Add a parity test that every LlmProviders value is either listed for Add Model or frozen in an explicit unlisted set, so a new backend provider cannot silently miss the dropdown again. Claude-Session: https://claude.ai/code/session_011Tn3657NkV6ojLqewL64Kb --- .../provider_create_fields.json | 7 ++ .../public_endpoints/test_public_endpoints.py | 86 +++++++++++++++++++ .../components/provider_info_helpers.test.tsx | 14 +++ .../src/components/provider_info_helpers.tsx | 4 + 4 files changed, 111 insertions(+) diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 66f8c2ea36f..cd781abee26 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -688,6 +688,13 @@ ], "default_model_placeholder": "gpt-3.5-turbo" }, + { + "provider": "CHATGPT", + "provider_display_name": "ChatGPT Subscription", + "litellm_provider": "chatgpt", + "credential_fields": [], + "default_model_placeholder": "chatgpt/gpt-5.4" + }, { "provider": "CLARIFAI", "provider_display_name": "Clarifai", diff --git a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py index 4a19ad3541c..fade7c9e7ee 100644 --- a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py @@ -1,5 +1,6 @@ import re from datetime import datetime, timezone +from typing import Final from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -327,6 +328,91 @@ def test_cognition_provider_fields(): assert fields_by_key["api_base"]["required"] is False +def test_chatgpt_provider_fields(): + """The ChatGPT subscription provider must be selectable in the Add Model flow (LIT-7127). + + Its backend signs in through the device-code auth file on the proxy host and ignores + api_key/api_base, so the entry carries no credential fields: any field here would be inert. + """ + app_instance = FastAPI() + app_instance.include_router(router) + test_client = TestClient(app_instance) + + response = test_client.get("/public/providers/fields") + assert response.status_code == 200 + providers = response.json() + + chatgpt = next((p for p in providers if p["provider"] == "CHATGPT"), None) + assert chatgpt is not None, "ChatGPT provider entry not found" + + assert chatgpt["provider_display_name"] == "ChatGPT Subscription" + assert chatgpt["litellm_provider"] == LlmProviders.CHATGPT.value + assert chatgpt["default_model_placeholder"].startswith("chatgpt/") + assert chatgpt["credential_fields"] == [] + + +ADD_MODEL_UNLISTED_PROVIDERS: Final = frozenset( + { + "a2a", + "a2a_agent", + "amazon_nova", + "apertis", + "aws_polly", + "black_forest_labs", + "charity_engine", + "chutes", + "darkbloom", + "gdc", + "helicone", + "inception", + "langflow", + "langgraph", + "libertai", + "litellm_agent", + "manus", + "meta", + "modelscope", + "mongodb", + "nano-gpt", + "neosantara", + "parasail", + "pinstripes", + "poe", + "publicai", + "ragflow", + "reducto", + "s3_vectors", + "sagemaker_nova", + "scaleway", + "stability", + "synthetic", + "tencent", + "tensormesh", + "text-completion-inception", + "valkey", + "xiaomi_mimo", + "zai", + } +) + + +def test_every_backend_provider_is_listed_in_add_model_or_frozen_as_unlisted(): + """A provider LiteLLM ships must be reachable from the Add Model dropdown, which is driven + entirely by /public/providers/fields (LIT-7127). Providers that predate this check are frozen + in ADD_MODEL_UNLISTED_PROVIDERS; a new provider gets a JSON entry rather than a line here. + """ + app_instance = FastAPI() + app_instance.include_router(router) + test_client = TestClient(app_instance) + + response = test_client.get("/public/providers/fields") + assert response.status_code == 200 + listed = {p["litellm_provider"] for p in response.json()} + + unlisted = {provider.value for provider in LlmProviders} - listed + assert unlisted == ADD_MODEL_UNLISTED_PROVIDERS + + def test_google_ai_studio_provider_fields_expose_api_base(): """The Google AI Studio (gemini) credential form must let admins set a custom api_base so they can point at a Gemini-compatible gateway (e.g. a self-hosted diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx index dfc737ddd45..4c68e302267 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx @@ -89,6 +89,16 @@ describe("provider_info_helpers", () => { expect(result.logo).toBe(providerLogoMap[Providers.BedrockMantle]); }); + it("should map the chatgpt slug and CHATGPT enum key to the ChatGPT Subscription name and OpenAI logo", () => { + const fromSlug = getProviderLogoAndName("chatgpt"); + expect(fromSlug.displayName).toBe("ChatGPT Subscription"); + expect(fromSlug.logo).toContain("openai_small"); + + const fromEnumKey = getProviderLogoAndName("CHATGPT"); + expect(fromEnumKey.displayName).toBe("ChatGPT Subscription"); + expect(fromEnumKey.logo).toContain("openai_small"); + }); + it("should handle provider values case-insensitively", () => { const result = getProviderLogoAndName("OPENAI"); expect(result.displayName).toBe(Providers.OpenAI); @@ -272,6 +282,10 @@ describe("provider_info_helpers", () => { expect(getPlaceholder(Providers.Cognition)).toBe("cognition/swe-1.7"); }); + it("should return a chatgpt/ placeholder for the CHATGPT dropdown key", () => { + expect(getPlaceholder("CHATGPT")).toBe("chatgpt/gpt-5.4"); + }); + it("should return default gpt-3.5-turbo placeholder for unknown provider", () => { expect(getPlaceholder("UnknownProvider" as any)).toBe("gpt-3.5-turbo"); }); diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index d01a6a34cbe..72ec5990557 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -84,6 +84,7 @@ export enum Providers { BASETEN = "Baseten", BYTEZ = "Bytez", Cerebras = "Cerebras", + CHATGPT = "ChatGPT Subscription", CLARIFAI = "Clarifai", CLOUDFLARE = "Cloudflare", CODESTRAL = "Codestral", @@ -198,6 +199,7 @@ export const provider_map: Record = { BedrockMantle: "bedrock_mantle", BYTEZ: "bytez", Cerebras: "cerebras", + CHATGPT: "chatgpt", CLARIFAI: "clarifai", CLOUDFLARE: "cloudflare", CODESTRAL: "codestral", @@ -314,6 +316,7 @@ export const providerLogoMap: Partial> = { [Providers.BedrockMantle]: bedrockLogo.src, [Providers.SageMaker]: bedrockLogo.src, [Providers.Cerebras]: cerebrasLogo.src, + [Providers.CHATGPT]: openaiSmallLogo.src, [Providers.CLOUDFLARE]: cloudflareLogo.src, [Providers.CODESTRAL]: mistralLogo.src, [Providers.Cohere]: cohereLogo.src, @@ -425,6 +428,7 @@ const providerPlaceholderMap: Partial> = { [Providers.Azure]: "my-deployment", [Providers.Azure_AI_Studio]: "azure_ai/command-r-plus", [Providers.Bedrock]: "claude-3-opus", + [Providers.CHATGPT]: "chatgpt/gpt-5.4", [Providers.Cognition]: "cognition/swe-1.7", [Providers.Cursor]: "cursor/claude-4-sonnet", [Providers.DeepInfra]: "deepinfra/", From bb2db2d3f8e218c6a781e029223a8af903e9d6dc Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Mon, 7 Sep 2026 15:12:41 -0700 Subject: [PATCH 2/2] test(proxy): drop docstrings from the Add Model provider tests Move the only guidance worth keeping into the parity assertion message so a failing run tells the contributor to add a catalog entry instead of growing the frozen unlisted set. Claude-Session: https://claude.ai/code/session_011Tn3657NkV6ojLqewL64Kb --- .../public_endpoints/test_public_endpoints.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py index fade7c9e7ee..0d82ed778f5 100644 --- a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py @@ -329,11 +329,6 @@ def test_cognition_provider_fields(): def test_chatgpt_provider_fields(): - """The ChatGPT subscription provider must be selectable in the Add Model flow (LIT-7127). - - Its backend signs in through the device-code auth file on the proxy host and ignores - api_key/api_base, so the entry carries no credential fields: any field here would be inert. - """ app_instance = FastAPI() app_instance.include_router(router) test_client = TestClient(app_instance) @@ -397,10 +392,6 @@ ADD_MODEL_UNLISTED_PROVIDERS: Final = frozenset( def test_every_backend_provider_is_listed_in_add_model_or_frozen_as_unlisted(): - """A provider LiteLLM ships must be reachable from the Add Model dropdown, which is driven - entirely by /public/providers/fields (LIT-7127). Providers that predate this check are frozen - in ADD_MODEL_UNLISTED_PROVIDERS; a new provider gets a JSON entry rather than a line here. - """ app_instance = FastAPI() app_instance.include_router(router) test_client = TestClient(app_instance) @@ -410,7 +401,10 @@ def test_every_backend_provider_is_listed_in_add_model_or_frozen_as_unlisted(): listed = {p["litellm_provider"] for p in response.json()} unlisted = {provider.value for provider in LlmProviders} - listed - assert unlisted == ADD_MODEL_UNLISTED_PROVIDERS + assert unlisted == ADD_MODEL_UNLISTED_PROVIDERS, ( + "Add Model dropdown drift: give the new provider an entry in provider_create_fields.json " + "rather than adding it to ADD_MODEL_UNLISTED_PROVIDERS" + ) def test_google_ai_studio_provider_fields_expose_api_base():