From 9d0466ba6dd91bb5bebaf135c640dd39d9d0e30e Mon Sep 17 00:00:00 2001 From: Jason Cook Date: Thu, 23 Apr 2026 11:21:02 -0400 Subject: [PATCH] feat(ui): oauth_credential_select field type for Add Model form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the ChatGPT (OAuth) provider in the Add Model form rendered a plain text input for ``api_key`` and expected the admin to type the ``oauth:`` marker by hand — easy to typo, and discoverability depended on reading the tooltip. Add a new ``field_type: "oauth_credential_select"`` that ``provider_specific_fields.tsx`` renders as an antd Select populated from ``useCredentials()``, filtered to OAuth-backed credentials (``credential_info.type`` in ``{chatgpt_oauth, copilot_oauth}``). Picking a row stores ``oauth:`` as the form value, so the downstream request path is unchanged. Wire-up: - ``ProviderCredentialField`` Literal (Python) and ``ProviderCredentialFieldMetadata`` union (TypeScript) both extended with the new type. - ``provider_create_fields.json`` for ``ChatGPT`` updated to use it. - Empty-state: if no OAuth credentials exist yet, the dropdown shows a "No OAuth credentials found" hint pointing at Credentials → Add Credential. No change to Copilot's entry — Copilot's existing ``api_key``/``api_base`` text fields stay, since that provider still supports plain PAT auth alongside OAuth. Admins who want the OAuth flow for Copilot can still type ``oauth:`` into the ``api_key`` field manually; the dropdown will cover that too in a follow-up if you want symmetry. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../provider_create_fields.json | 10 ++-- .../public_endpoints/public_endpoints.py | 9 +++- .../add_model/provider_specific_fields.tsx | 50 +++++++++++++++++-- .../src/components/networking.tsx | 8 ++- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 6aca902bdf5..10221b79ff1 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -577,13 +577,13 @@ "credential_fields": [ { "key": "api_key", - "label": "OAuth credential reference", - "placeholder": "oauth:my-chatgpt", - "tooltip": "Reference a stored ChatGPT OAuth credential by name, prefixed with 'oauth:'. Create the credential first via Credentials \u2192 Add Credential \u2192 ChatGPT (OAuth).", + "label": "OAuth credential", + "placeholder": "Select a stored ChatGPT OAuth credential", + "tooltip": "Pick a credential you signed in with via Credentials \u2192 Add Credential \u2192 ChatGPT (OAuth). The proxy resolves the chosen name to the stored OAuth tokens at request time.", "required": true, - "field_type": "text", + "field_type": "oauth_credential_select", "options": null, - "default_value": "oauth:" + "default_value": null } ], "default_model_placeholder": "gpt-5.3-codex" diff --git a/litellm/types/proxy/public_endpoints/public_endpoints.py b/litellm/types/proxy/public_endpoints/public_endpoints.py index caa9a978530..dfe63b053fa 100644 --- a/litellm/types/proxy/public_endpoints/public_endpoints.py +++ b/litellm/types/proxy/public_endpoints/public_endpoints.py @@ -19,7 +19,14 @@ class ProviderCredentialField(BaseModel): placeholder: Optional[str] = None tooltip: Optional[str] = None required: bool = False - field_type: Literal["text", "password", "select", "upload", "textarea"] = "text" + field_type: Literal[ + "text", + "password", + "select", + "upload", + "textarea", + "oauth_credential_select", + ] = "text" options: Optional[List[str]] = None default_value: Optional[str] = None diff --git a/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx b/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx index 24df0ac21ef..5bd350e00c1 100644 --- a/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx +++ b/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx @@ -1,3 +1,4 @@ +import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; import { useProviderFields } from "@/app/(dashboard)/hooks/providers/useProviderFields"; import { UploadOutlined } from "@ant-design/icons"; import { Text, TextInput } from "@tremor/react"; @@ -5,6 +6,10 @@ import { Button as Button2, Col, Form, Input, Row, Select, Typography, Upload, U import React from "react"; import { CredentialItem, ProviderCredentialFieldMetadata } from "../networking"; import { provider_map, Providers } from "../provider_info_helpers"; + +// credential_info.type values recognised as OAuth-backed credentials. +// Add more as we ship OAuth flows for other providers. +const OAUTH_CREDENTIAL_TYPES = new Set(["chatgpt_oauth", "copilot_oauth"]); const { Link } = Typography; interface ProviderSpecificFieldsProps { @@ -18,7 +23,13 @@ interface ProviderCredentialField { placeholder?: string; tooltip?: string; required?: boolean; - type?: "text" | "password" | "select" | "upload" | "textarea"; + type?: + | "text" + | "password" + | "select" + | "upload" + | "textarea" + | "oauth_credential_select"; options?: string[]; defaultValue?: string; } @@ -38,7 +49,9 @@ const mapFieldMetadataToUiField = (field: ProviderCredentialFieldMetadata): Prov ? "upload" : field.field_type === "textarea" ? "textarea" - : "text"; + : field.field_type === "oauth_credential_select" + ? "oauth_credential_select" + : "text"; return { key: field.key, @@ -98,6 +111,17 @@ const ProviderSpecificFields: React.FC = ({ selecte const { data: providerMetadata, isLoading, error: loadError } = useProviderFields(); + // Fetched lazily; only used by oauth_credential_select fields. The hook + // is a no-op until ``accessToken`` is available. + const { data: credentialsResponse } = useCredentials(); + const oauthCredentials = React.useMemo(() => { + const all = credentialsResponse?.credentials ?? []; + return all.filter((c: CredentialItem) => { + const type = (c.credential_info as Record | undefined)?.type; + return typeof type === "string" && OAUTH_CREDENTIAL_TYPES.has(type); + }); + }, [credentialsResponse]); + // Memoize the expensive cache computation const cacheEntries = React.useMemo(() => { if (!providerMetadata) { @@ -223,7 +247,27 @@ const ProviderSpecificFields: React.FC = ({ selecte tooltip={field.tooltip} className={field.key === "vertex_credentials" ? "mb-0" : undefined} > - {field.type === "select" ? ( + {field.type === "oauth_credential_select" ? ( + + ) : field.type === "select" ? (