Use a free-text model id for providers without an enumerable model list
This commit is contained in:
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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user