Use a free-text model id for providers without an enumerable model list

This commit is contained in:
ZacharyZhang-NY committed 2026-05-30 01:27:38 -04:00
1 parent d4ace283d1
commit 2dbab2c1fe
5 files changed
+29 -5

No files matched your search

@@ -297,6 +297,7 @@ function ProviderCredentialFields({
}) {
const isOAuthLike = selected.authType === "oauth_pkce" || selected.authType === "account_subscription";
const isCustom = Boolean(selected.customEndpoint);
const live = selected.supportsLiveModels;
const [fetched, setFetched] = useState<ProviderCatalogEntry["models"] | null>(null);
const [loading, setLoading] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
@@ -340,7 +341,7 @@ function ProviderCredentialFields({
buildInput={() => ({ providerKey: selected.key })}
onComplete={(secret) => {
onChange({ providerSecret: secret ?? "" });
if (secret) void loadModels(secret);
if (secret && live) void loadModels(secret);
}}
/>
) : isCustom ? (
@@ -374,7 +375,7 @@ function ProviderCredentialFields({
/>
</Field>
)}
{!isOAuthLike && (
{!isOAuthLike && live && (
<Button tone="ghost" onClick={loadTypedModels} disabled={!hasCredential || loading}>
{loading ? "Getting models…" : fetched ? "Reload models" : "Get models"}
</Button>
@@ -396,7 +397,7 @@ function ProviderCredentialFields({
<input
className="input"
value={draft.providerModel}
placeholder={isOAuthLike ? "Loading…" : "Click “Get models”, or type a model id"}
placeholder={modelPlaceholder(live, isOAuthLike, loading)}
onChange={(event) => onChange({ providerModel: event.target.value })}
/>
)}
@@ -406,6 +407,12 @@ function ProviderCredentialFields({
);
}
function modelPlaceholder(live: boolean, isOAuthLike: boolean, loading: boolean): string {
if (!live) return "Type the model id (e.g. gpt-5.5)";
if (loading || isOAuthLike) return "Loading…";
return "Click “Get models”, or type a model id";
}
function authBadge(authType: string): string {
if (authType === "api_key") return "API key";
if (authType === "oauth_pkce") return "OAuth";
@@ -359,7 +359,7 @@ function ModelPicker({ entry, provider }: { entry: ProviderCatalogEntry; provide
const [error, setError] = useState<string | null>(null);
const [value, setValue] = useState(provider.model ?? entry.defaultModel ?? entry.models[0]?.id ?? "");
const models = fetched ?? entry.models;
const connected = Boolean(provider.credentialId) && provider.enabled && provider.authState === "configured";
const connected = entry.supportsLiveModels && Boolean(provider.credentialId) && provider.enabled && provider.authState === "configured";
const refresh = async () => {
setLoading(true);
+4
View File
@@ -292,6 +292,10 @@ export interface ProviderCatalogEntry {
signInUrl: string | null;
credentialHelp: string;
models: ProviderCatalogModel[];
/** True when the provider exposes a real models endpoint we can fetch from.
* When false (e.g. the Codex subscription, which has no enumerable list), the
* UI offers a free-text model id instead of a fixed dropdown. */
supportsLiveModels: boolean;
/** Set for user-configured OpenAI/Anthropic-compatible endpoints: the UI
* collects a base URL + API key + model instead of a fixed provider form. */
customEndpoint?: "openai" | "anthropic";
@@ -1,7 +1,7 @@
import { PROVIDER_AUTH_TYPE, type ProviderCatalogEntry } from "./domain";
import { listProviderModels } from "./provider-models";
const RAW_CATALOG: Omit<ProviderCatalogEntry, "models">[] = [
const RAW_CATALOG: Omit<ProviderCatalogEntry, "models" | "supportsLiveModels">[] = [
{
key: "openai",
label: "OpenAI",
@@ -150,7 +150,19 @@ const RAW_CATALOG: Omit<ProviderCatalogEntry, "models">[] = [
},
];
// Providers with no enumerable models endpoint: the Codex subscription and the
// browser-token / cloud connectors. Their model id is entered as free text.
const CURATED_MODEL_PROVIDERS = new Set([
"github-copilot",
"google-antigravity",
"gpt-account",
"azure-openai",
"aws-bedrock",
"vertex-ai",
]);
export const PROVIDER_CATALOG: ProviderCatalogEntry[] = RAW_CATALOG.map((entry) => ({
...entry,
models: listProviderModels(entry.key),
supportsLiveModels: !CURATED_MODEL_PROVIDERS.has(entry.key),
}));
@@ -98,6 +98,7 @@ describe("provider contracts", () => {
const adapter = providerAdapters[provider.key];
const authProfile = adapter.parseAuthProfile(PROVIDER_SECRET_SAMPLES[provider.key]);
expect(typeof adapter.listModels).toBe("function");
expect(provider.supportsLiveModels).toBe(!CURATED_MODEL_PROVIDERS.has(provider.key));
if (CURATED_MODEL_PROVIDERS.has(provider.key)) {
await expect(adapter.listModels({ authProfile, model: provider.defaultModel })).resolves.toEqual(provider.models);
}