import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { THINKING_LEVELS } from "@fusion/core"; import type { Settings, ThinkingLevel } from "@fusion/core"; import type { ModelInfo } from "../../../api"; import type { ToastType } from "../../../hooks/useToast"; import { ModelPricingSection } from "./ModelPricingSection"; import { CustomModelDropdown } from "../../CustomModelDropdown"; import type { SectionBaseProps, ModelLane } from "./context"; import { LoadingSpinner } from "../../LoadingSpinner"; function toCommaSeparatedInput(values?: string[]): string { return values?.join(", ") ?? ""; } function fromCommaSeparatedInput(value: string): string[] { return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0); } export interface GlobalModelsSectionProps extends SectionBaseProps { scopeBanner: ReactNode; availableModels: ModelInfo[]; modelsLoading: boolean; /** Global model lanes (i.e. MODEL_LANES without the `default` lane). */ globalModelLanes: ModelLane[]; favoriteProviders: string[]; favoriteModels: string[]; onToggleFavorite: (provider: string) => void; onToggleModelFavorite: (modelId: string) => void; addToast: (message: string, type?: ToastType) => void; projectId?: string; } export function GlobalModelsSection({ scopeBanner, form, setForm, availableModels, modelsLoading, globalModelLanes, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, addToast, projectId, }: GlobalModelsSectionProps) { const { t } = useTranslation("app"); const selectedValue = form.defaultProvider && form.defaultModelId ? `${form.defaultProvider}/${form.defaultModelId}` : ""; return (<> {scopeBanner} {/* --- Default Model --- */}

{t("settings.globalModels.defaultModel", "Default Model")}

{modelsLoading ? (
) : availableModels.length === 0 ? (
{t("settings.models.noModels", "No models available. Configure authentication first.")}
) : (<>
{ if (!val) { setForm((f) => ({ ...f, defaultProvider: undefined, defaultModelId: undefined })); } else { const slashIdx = val.indexOf("/"); setForm((f) => ({ ...f, defaultProvider: val.slice(0, slashIdx), defaultModelId: val.slice(slashIdx + 1), })); } }} placeholder={t("settings.globalModels.useDefault", "Use default")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/> {t("settings.globalModels.defaultAIModelUsedForTaskExecutionWhen", "Default AI model used for task execution when no per-task override is set. "Use default" lets the engine choose automatically.")}
{ if (!val) { setForm((f) => ({ ...f, fallbackProvider: undefined, fallbackModelId: undefined })); } else { const slashIdx = val.indexOf("/"); setForm((f) => ({ ...f, fallbackProvider: val.slice(0, slashIdx), fallbackModelId: val.slice(slashIdx + 1), })); } }} placeholder={t("settings.globalModels.noFallback", "No fallback")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/> {t("settings.globalModels.usedAutomaticallyIfThePrimaryDefaultModelHits", "Used automatically if the primary default model hits a retryable provider error like rate limiting or overload.")}
)} {(() => { const selectedModel = availableModels.find((m) => m.provider === form.defaultProvider && m.id === form.defaultModelId); if (selectedModel && !selectedModel.reasoning) return null; return (
{/* FNXC:Settings-ThinkingLevel 2026-06-19-14:55: This global selector renders the canonical THINKING_LEVELS list so newly added `xhigh` stays available anywhere the default reasoning effort is configured. */} {t("settings.globalModels.controlsHowMuchReasoningEffortTheAIModel", "Controls how much reasoning effort the AI model uses. Higher levels produce better results but cost more.")}
); })()} {availableModels.length > 0 && (<>

{t("settings.globalModels.modelLanes", "Model Lanes")}

{t("settings.globalModels.globalBaselineModelsForEachAIRoleProject", " Global baseline models for each AI role. Project settings can override these per-project. ")}

{globalModelLanes.map((lane) => { const provider = form[lane.globalProviderKey as keyof Settings] as string | undefined; const model = form[lane.globalModelKey as keyof Settings] as string | undefined; const value = provider && model ? `${provider}/${model}` : ""; return (
{ if (!selected) { setForm((f) => ({ ...f, [lane.globalProviderKey]: undefined, [lane.globalModelKey]: undefined, })); return; } const slashIdx = selected.indexOf("/"); setForm((f) => ({ ...f, [lane.globalProviderKey]: selected.slice(0, slashIdx), [lane.globalModelKey]: selected.slice(slashIdx + 1), })); }} placeholder={t("settings.globalModels.useDefault", "Use default")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/> {lane.helperText}
); })} )} {/* --- Startup Model Sync --- */}

{t("settings.globalModels.startupModelSync", "Startup Model Sync")}

{t("settings.globalModels.whenEnabledStartupFetchesTheLatestAvailableModels", " When enabled, startup fetches the latest available models from the OpenRouter API so model pickers always include the newest catalog. ")}
{t("settings.globalModels.whenEnabledStartupRefreshesModelsThroughTheLocal", " When enabled, startup refreshes models through the local ")}opencode models opencode --refresh{t("settings.globalModels.flowAndPublishesThemUnderTheOpencodeGo", " flow and publishes them under the opencode-go provider in model pickers. ")}
{t("settings.globalModels.openRouterAdvanced", "OpenRouter advanced")}
setForm((f) => ({ ...f, openrouterAppAttribution: { ...(f.openrouterAppAttribution || {}), referer: e.target.value, }, }))}/> {t("settings.globalModels.leaveEmptyToOmitThisHeaderDefaultHttps", "Leave empty to omit this header. Default: https://runfusion.ai.")}
setForm((f) => ({ ...f, openrouterAppAttribution: { ...(f.openrouterAppAttribution || {}), title: e.target.value, }, }))}/> {t("settings.globalModels.leaveEmptyToOmitThisHeaderDefaultFusion", "Leave empty to omit this header. Default: Fusion.")}
{ const parsed = fromCommaSeparatedInput(e.target.value); setForm((f) => ({ ...f, openrouterModelFilters: { ...(f.openrouterModelFilters || {}), supported_parameters: parsed.length > 0 ? parsed : undefined, }, })); }}/> {t("settings.globalModels.commaSeparatedValuesSentToOpenRouterModelSync", "Comma-separated values sent to OpenRouter model sync.")}
{ const parsed = fromCommaSeparatedInput(e.target.value); setForm((f) => ({ ...f, openrouterModelFilters: { ...(f.openrouterModelFilters || {}), output_modalities: parsed.length > 0 ? parsed : undefined, }, })); }}/> {t("settings.globalModels.commaSeparatedValuesSentToOpenRouterModelSync", "Comma-separated values sent to OpenRouter model sync.")}
{ const parsed = fromCommaSeparatedInput(e.target.value); setForm((f) => ({ ...f, openrouterProviderPreferences: { ...(f.openrouterProviderPreferences || {}), order: parsed.length > 0 ? parsed : undefined, }, })); }}/>
{ const parsed = fromCommaSeparatedInput(e.target.value); setForm((f) => ({ ...f, openrouterProviderPreferences: { ...(f.openrouterProviderPreferences || {}), ignore: parsed.length > 0 ? parsed : undefined, }, })); }}/>
{ const parsed = fromCommaSeparatedInput(e.target.value); setForm((f) => ({ ...f, openrouterProviderPreferences: { ...(f.openrouterProviderPreferences || {}), only: parsed.length > 0 ? parsed : undefined, }, })); }}/>
); } export default GlobalModelsSection;