From fe1107773c10b273b33026697af4bb8fed8a51d3 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 30 Sep 2026 08:10:38 -0700 Subject: [PATCH 1/2] feat(web): provider model picker from a per-provider config map (CL-9555) --- .../src/onboarding/provider-connect-step.tsx | 103 +++++++++++++----- apps/web/src/settings/provider-models.ts | 33 ++++++ 2 files changed, 111 insertions(+), 25 deletions(-) create mode 100644 apps/web/src/settings/provider-models.ts diff --git a/apps/web/src/onboarding/provider-connect-step.tsx b/apps/web/src/onboarding/provider-connect-step.tsx index 022086f3a..5f1ccf0e7 100644 --- a/apps/web/src/onboarding/provider-connect-step.tsx +++ b/apps/web/src/onboarding/provider-connect-step.tsx @@ -20,6 +20,8 @@ import type { FormEvent } from "react"; import type { ModelProviderPlugin } from "@intx/types"; +import { PROVIDER_MODELS } from "@/settings/provider-models"; + import { fetchOllamaTags } from "./ollama-tags"; import "./provider-rows.css"; @@ -45,6 +47,8 @@ export type ProviderOption = { readonly customURL?: boolean; }; +const CUSTOM_MODEL = "__custom__"; + // https everywhere; plain http only for a server on this machine. const baseUrlSchema = type("string").narrow((value, ctx) => { let url: URL; @@ -229,6 +233,8 @@ export function ProviderConnectStep({ const [apiKey, setApiKey] = useState(""); const [baseURL, setBaseURL] = useState(""); const [modelName, setModelName] = useState(""); + // "" = the provider's default (first listed); CUSTOM_MODEL = typed in. + const [modelChoice, setModelChoice] = useState(""); const [submitting, setSubmitting] = useState(false); const [loginId, setLoginId] = useState(null); @@ -238,6 +244,20 @@ export function ProviderConnectStep({ const oauthProvider = option?.oauthProvider; const typedModel = option?.typedModel === true; const customURL = option?.customURL === true; + const models = option === undefined ? [] : (PROVIDER_MODELS[option.id] ?? []); + const listed = models.length > 0; + const choice = modelChoice === "" ? (models[0]?.id ?? CUSTOM_MODEL) : modelChoice; + const typedChoice = choice === CUSTOM_MODEL; + // The model this option will mint: a listed pick, else the typed name. + const resolvedModel = + option === undefined + ? "" + : listed && !typedChoice + ? choice + : isLocal || typedModel || listed + ? modelName.trim() + : option.canonicalName; + const resolvedLabel = models.find((model) => model.id === resolvedModel)?.label ?? resolvedModel; const urlCheck = baseUrlSchema(baseURL); const urlError = customURL && baseURL.trim() !== "" && urlCheck instanceof type.errors; @@ -257,13 +277,12 @@ export function ProviderConnectStep({ const ready = option !== undefined && + resolvedModel !== "" && (oauthProvider !== undefined || (isLocal ? baseURL.trim().length > 0 && modelName.trim().length > 0 && modelKnown : typedModel - ? apiKey.trim().length > 0 && - modelName.trim().length > 0 && - (!customURL || !(urlCheck instanceof type.errors)) + ? apiKey.trim().length > 0 && (!customURL || !(urlCheck instanceof type.errors)) : apiKey.trim().length > 0)); function fail(cause: unknown, operation: string) { @@ -286,6 +305,7 @@ export function ProviderConnectStep({ setSelected(id); setBaseURL(next?.local === true ? next.baseURL : ""); setModelName(""); + setModelChoice(""); } // Starting a login is the hub's job end to end: it runs the loopback PKCE @@ -323,8 +343,8 @@ export function ProviderConnectStep({ const state = await readProviderLogin(tenantId, loginId); if (state.status !== "completed") return state; const created = await shadowOffering(tenantId, { - canonicalName: option.canonicalName, - modelDisplayName: option.modelDisplayName, + canonicalName: resolvedModel, + modelDisplayName: resolvedLabel, providerName: option.label, plugin: option.plugin, baseURL: option.baseURL, @@ -333,7 +353,7 @@ export function ProviderConnectStep({ }); return { status: "connected" as const, - offering: offeringFromOption(option, option.canonicalName, created.id), + offering: offeringFromOption(option, resolvedModel, created.id), }; }, refetchInterval: (query) => (query.state.data?.status === "pending" ? 2000 : false), @@ -365,11 +385,10 @@ export function ProviderConnectStep({ } setSubmitting(true); try { - const typed = isLocal || typedModel; - const canonicalName = typed ? modelName.trim() : option.canonicalName; + const canonicalName = resolvedModel; const created = await shadowOffering(tenantId, { canonicalName, - modelDisplayName: typed ? modelName.trim() : option.modelDisplayName, + modelDisplayName: resolvedLabel, providerName: option.label, plugin: option.plugin, baseURL: isLocal || customURL ? baseURL.trim() : option.baseURL, @@ -394,6 +413,47 @@ export function ProviderConnectStep({ ? "Connecting…" : "Connect"; + const modelPicker = listed ? ( + <> + + {typedChoice ? ( + + ) : null} + + ) : ( + + ); + return (
void handleSubmit(event)}> {option === undefined ? null : oauthProvider !== undefined ? ( -

- {waiting - ? "Finish signing in on the tab that opened, then come back here." - : "A new tab opens to sign in. Your workbench stores the result; no key to paste."} -

+ <> + {modelPicker} +

+ {waiting + ? "Finish signing in on the tab that opened, then come back here." + : "A new tab opens to sign in. Your workbench stores the result; no key to paste."} +

+ ) : isLocal ? ( <> - {typedModel ? ( - - ) : null} + {typedModel || listed ? modelPicker : null} )}
diff --git a/apps/web/src/settings/provider-models.ts b/apps/web/src/settings/provider-models.ts new file mode 100644 index 000000000..c597a7963 --- /dev/null +++ b/apps/web/src/settings/provider-models.ts @@ -0,0 +1,33 @@ +// Models offered per provider kind, by `ProviderOption.id`. The first entry is +// the preselected default; an empty list means the model is typed in. This +// moves to a package later. +export type ProviderModel = { readonly id: string; readonly label: string }; + +export const PROVIDER_MODELS: Readonly> = { + anthropic: [ + { id: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" }, + { id: "claude-opus-4-1", label: "Claude Opus 4.1" }, + { id: "claude-haiku-4-5", label: "Claude Haiku 4.5" }, + ], + openai: [ + { id: "gpt-5", label: "GPT-5" }, + { id: "gpt-5-mini", label: "GPT-5 mini" }, + { id: "gpt-4.1", label: "GPT-4.1" }, + ], + openrouter: [ + { id: "anthropic/claude-sonnet-4.5", label: "Claude Sonnet 4.5" }, + { id: "openai/gpt-5", label: "GPT-5" }, + { id: "google/gemini-2.5-pro", label: "Gemini 2.5 Pro" }, + { id: "x-ai/grok-4", label: "Grok 4" }, + { id: "deepseek/deepseek-chat-v3.1", label: "DeepSeek V3.1" }, + ], + xai: [ + { id: "grok-4", label: "Grok 4" }, + { id: "grok-4-fast", label: "Grok 4 Fast" }, + ], + codex: [ + { id: "gpt-5.5", label: "GPT-5.5" }, + { id: "gpt-5", label: "GPT-5" }, + ], + custom: [], +}; From 07e26d58f82dc1dcf9d9908374f169d4b34334e3 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 30 Sep 2026 08:13:37 -0700 Subject: [PATCH 2/2] fix(web): keep each provider's previous default model first (CL-9555) --- apps/web/src/settings/provider-models.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/web/src/settings/provider-models.ts b/apps/web/src/settings/provider-models.ts index c597a7963..21a865907 100644 --- a/apps/web/src/settings/provider-models.ts +++ b/apps/web/src/settings/provider-models.ts @@ -6,7 +6,8 @@ export type ProviderModel = { readonly id: string; readonly label: string }; export const PROVIDER_MODELS: Readonly> = { anthropic: [ { id: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" }, - { id: "claude-opus-4-1", label: "Claude Opus 4.1" }, + { id: "claude-sonnet-5-5", label: "Claude Sonnet 5.5" }, + { id: "claude-opus-5-5", label: "Claude Opus 5.5" }, { id: "claude-haiku-4-5", label: "Claude Haiku 4.5" }, ], openai: [ @@ -19,15 +20,12 @@ export const PROVIDER_MODELS: Readonly> { id: "openai/gpt-5", label: "GPT-5" }, { id: "google/gemini-2.5-pro", label: "Gemini 2.5 Pro" }, { id: "x-ai/grok-4", label: "Grok 4" }, - { id: "deepseek/deepseek-chat-v3.1", label: "DeepSeek V3.1" }, ], xai: [ + { id: "grok-4.6", label: "Grok 4.6" }, { id: "grok-4", label: "Grok 4" }, { id: "grok-4-fast", label: "Grok 4 Fast" }, ], - codex: [ - { id: "gpt-5.5", label: "GPT-5.5" }, - { id: "gpt-5", label: "GPT-5" }, - ], + codex: [{ id: "gpt-5.5", label: "GPT-5.5" }], custom: [], };