Show custom OpenAI/Anthropic-compatible providers in onboarding and settings

This commit is contained in:
ZacharyZhang-NY committed 2026-05-29 22:56:01 -04:00
1 parent 4d2425dde0
commit 9996b89f72
5 files changed
+60 -4

No files matched your search

@@ -85,7 +85,7 @@ export class OnboardingService {
private resolveProvider(providerKey: string) {
const provider = PROVIDER_CATALOG.find((entry) => entry.key === providerKey);
if (!provider) throw new Error(`Provider ${providerKey} is unavailable.`);
if (provider.authType === PROVIDER_AUTH_TYPE.CLOUD) {
if (provider.authType === PROVIDER_AUTH_TYPE.CLOUD && !provider.customEndpoint) {
throw new Error("Cloud providers need extended configuration — connect them from Settings after the company is created.");
}
return provider;
@@ -18,6 +18,7 @@ interface Draft {
executionMode: "ai_ceo" | "ai_cofounder";
providerKey: string;
providerSecret: string;
providerBaseUrl: string;
providerModel: string;
goalTitle: string;
goalDescription: string;
@@ -43,7 +44,7 @@ export function Onboarding({
onCompleted: () => void;
}) {
const availableProviders = useMemo(
() => snapshot.providerCatalog.filter((entry) => entry.authType !== PROVIDER_AUTH_TYPE.CLOUD),
() => snapshot.providerCatalog.filter((entry) => entry.authType !== PROVIDER_AUTH_TYPE.CLOUD || Boolean(entry.customEndpoint)),
[snapshot.providerCatalog],
);
const firstProvider = availableProviders[0];
@@ -58,6 +59,7 @@ export function Onboarding({
executionMode: "ai_ceo",
providerKey: firstProvider.key,
providerSecret: "",
providerBaseUrl: "",
providerModel: firstProvider.defaultModel ?? firstProvider.models[0]?.id ?? "",
goalTitle: "",
goalDescription: "",
@@ -93,6 +95,9 @@ export function Onboarding({
}
setSubmitting(true);
setError(null);
const providerSecret = selectedProvider.customEndpoint
? JSON.stringify({ baseUrl: draft.providerBaseUrl.trim(), secret: draft.providerSecret.trim() })
: draft.providerSecret.trim();
const result = await window.agentCompany.bootstrapOnboarding({
companyName: draft.companyName.trim(),
companyDescription: draft.companyDescription.trim(),
@@ -104,7 +109,7 @@ export function Onboarding({
goalTitle: draft.goalTitle.trim(),
goalDescription: draft.goalDescription.trim(),
providerKey: draft.providerKey,
providerSecret: draft.providerSecret.trim(),
providerSecret,
providerModel: draft.providerModel.trim(),
});
setSubmitting(false);
@@ -245,6 +250,7 @@ function ProviderStep({
const selected = providers.find((entry) => entry.key === draft.providerKey) ?? providers[0];
if (!selected) throw new Error("No provider available for onboarding.");
const isOAuthLike = selected.authType === "oauth_pkce" || selected.authType === "account_subscription";
const isCustom = Boolean(selected.customEndpoint);
return (
<div className="form-grid">
@@ -262,6 +268,7 @@ function ProviderStep({
providerKey: provider.key,
providerModel: provider.defaultModel ?? provider.models[0]?.id ?? "",
providerSecret: "",
providerBaseUrl: "",
})}
>
<div className="row">
@@ -283,6 +290,26 @@ function ProviderStep({
buildInput={() => ({ providerKey: selected.key })}
onComplete={(secret) => onChange({ providerSecret: secret ?? "" })}
/>
) : isCustom ? (
<>
<Field label="Base URL">
<input
className="input"
value={draft.providerBaseUrl}
placeholder={selected.customEndpoint === "openai" ? "https://host/v1" : "https://host"}
onChange={(event) => onChange({ providerBaseUrl: event.target.value })}
/>
</Field>
<Field label="API key">
<input
className="input"
type="password"
autoComplete="off"
value={draft.providerSecret}
onChange={(event) => onChange({ providerSecret: event.target.value })}
/>
</Field>
</>
) : (
<Field label="API key">
<input
@@ -434,8 +461,9 @@ function validateProvider(draft: Draft, providers: ProviderCatalogEntry[]): stri
if (!draft.providerKey) return "Pick a provider.";
const selected = providers.find((entry) => entry.key === draft.providerKey);
if (!selected) return "Provider is no longer available.";
if (selected.customEndpoint && !draft.providerBaseUrl.trim()) return "Base URL is required.";
if (!draft.providerSecret.trim()) {
return selected.authType === "api_key" ? "API key is required." : "Access token is required.";
return selected.authType === "api_key" || selected.customEndpoint ? "API key is required." : "Access token is required.";
}
if (!draft.providerModel.trim()) return "Model is required.";
return null;
@@ -288,9 +288,32 @@ function ProviderConnectFields({
</>
);
}
if (entry.customEndpoint) {
return <CompatFieldsForm entry={entry} provider={provider} />;
}
return <CloudFieldsForm entry={entry} provider={provider} />;
}
function CompatFieldsForm({ entry, provider }: { entry: ProviderCatalogEntry; provider: ProviderRecord }) {
const [baseUrl, setBaseUrl] = useState("");
return (
<>
<p style={{ fontSize: 12.5, color: "var(--text-3)", lineHeight: 1.5, margin: 0 }}>{entry.credentialHelp}</p>
<Field label="Base URL">
<input
className="input"
name="cloudBaseUrl"
value={baseUrl}
placeholder={entry.customEndpoint === "openai" ? "https://host/v1" : "https://host"}
onChange={(event) => setBaseUrl(event.target.value)}
/>
</Field>
<Field label="API key"><input className="input" name="secret" type="password" required /></Field>
<ModelPicker entry={entry} provider={provider} />
</>
);
}
function ProviderOAuthFields({
orgId,
employeeId,
+3
View File
@@ -292,6 +292,9 @@ export interface ProviderCatalogEntry {
signInUrl: string | null;
credentialHelp: string;
models: ProviderCatalogModel[];
/** 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";
}
export interface SkillDefinitionRecord {
@@ -136,6 +136,7 @@ const RAW_CATALOG: Omit<ProviderCatalogEntry, "models">[] = [
capabilityTags: ["custom", "openai-compatible"],
signInUrl: null,
credentialHelp: "Set the base URL (ending in /v1) and an API key for any OpenAI Chat Completions–compatible endpoint, then enter the model id.",
customEndpoint: "openai",
},
{
key: "custom-anthropic",
@@ -145,6 +146,7 @@ const RAW_CATALOG: Omit<ProviderCatalogEntry, "models">[] = [
capabilityTags: ["custom", "anthropic-compatible"],
signInUrl: null,
credentialHelp: "Set the base URL and an API key for any Anthropic Messages–compatible endpoint, then enter the model id.",
customEndpoint: "anthropic",
},
];