diff --git a/AgentCompany-New/src/main/services/onboarding-service.ts b/AgentCompany-New/src/main/services/onboarding-service.ts index dcd64681..cfb47d60 100644 --- a/AgentCompany-New/src/main/services/onboarding-service.ts +++ b/AgentCompany-New/src/main/services/onboarding-service.ts @@ -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; diff --git a/AgentCompany-New/src/renderer/src/app/Onboarding.tsx b/AgentCompany-New/src/renderer/src/app/Onboarding.tsx index facd9e52..ba1a2ffd 100644 --- a/AgentCompany-New/src/renderer/src/app/Onboarding.tsx +++ b/AgentCompany-New/src/renderer/src/app/Onboarding.tsx @@ -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 (
@@ -262,6 +268,7 @@ function ProviderStep({ providerKey: provider.key, providerModel: provider.defaultModel ?? provider.models[0]?.id ?? "", providerSecret: "", + providerBaseUrl: "", })} >
@@ -283,6 +290,26 @@ function ProviderStep({ buildInput={() => ({ providerKey: selected.key })} onComplete={(secret) => onChange({ providerSecret: secret ?? "" })} /> + ) : isCustom ? ( + <> + + onChange({ providerBaseUrl: event.target.value })} + /> + + + onChange({ providerSecret: event.target.value })} + /> + + ) : ( 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; diff --git a/AgentCompany-New/src/renderer/src/pages/settings/ProvidersPanel.tsx b/AgentCompany-New/src/renderer/src/pages/settings/ProvidersPanel.tsx index eebe16e0..6cd94b1a 100644 --- a/AgentCompany-New/src/renderer/src/pages/settings/ProvidersPanel.tsx +++ b/AgentCompany-New/src/renderer/src/pages/settings/ProvidersPanel.tsx @@ -288,9 +288,32 @@ function ProviderConnectFields({ ); } + if (entry.customEndpoint) { + return ; + } return ; } +function CompatFieldsForm({ entry, provider }: { entry: ProviderCatalogEntry; provider: ProviderRecord }) { + const [baseUrl, setBaseUrl] = useState(""); + return ( + <> +

{entry.credentialHelp}

+ + setBaseUrl(event.target.value)} + /> + + + + + ); +} + function ProviderOAuthFields({ orgId, employeeId, diff --git a/AgentCompany-New/src/shared/domain.ts b/AgentCompany-New/src/shared/domain.ts index 21509a94..86e48c72 100644 --- a/AgentCompany-New/src/shared/domain.ts +++ b/AgentCompany-New/src/shared/domain.ts @@ -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 { diff --git a/AgentCompany-New/src/shared/provider-catalog.ts b/AgentCompany-New/src/shared/provider-catalog.ts index 694f5bef..d5a2be3a 100644 --- a/AgentCompany-New/src/shared/provider-catalog.ts +++ b/AgentCompany-New/src/shared/provider-catalog.ts @@ -136,6 +136,7 @@ const RAW_CATALOG: Omit[] = [ 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[] = [ 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", }, ];