Add TKMind platform extensions, H5/MindSpace stack, and deployment tooling.
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled

Fork goose with custom MCP widgets, platform extensions (aider, git, web, search),
MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-14 21:30:20 +08:00
parent e5fd568e01
commit 4e21ca937a
359 changed files with 70658 additions and 56 deletions
-4
View File
@@ -1,4 +0,0 @@
VITE_START_EMBEDDED_SERVER=yes
GOOSE_PROVIDER__TYPE=openai
GOOSE_PROVIDER__HOST=https://api.openai.com
GOOSE_PROVIDER__MODEL=gpt-4o
+1 -2
View File
@@ -30,7 +30,7 @@ import SettingsView, { SettingsViewOptions } from './components/settings/Setting
import SessionsView from './components/sessions/SessionsView';
import SharedSessionView from './components/sessions/SharedSessionView';
import SchedulesView from './components/schedule/SchedulesView';
import ProviderSettings from './components/settings/providers/ProviderSettingsPage';
import ProviderSettings from './components/settings/providers/LLMProviderPage';
import { AppLayout } from './components/Layout/AppLayout';
import { ChatProvider, DEFAULT_CHAT_TITLE } from './contexts/ChatContext';
import LauncherView from './components/LauncherView';
@@ -260,7 +260,6 @@ const ConfigureProvidersRoute = () => {
<div className="w-screen h-screen bg-background-primary">
<ProviderSettings
onClose={() => navigate('/settings', { state: { section: 'models' } })}
isOnboarding={false}
/>
</div>
);
+54
View File
@@ -42,5 +42,59 @@
"enabled": false,
"type": "builtin",
"env_keys": []
},
{
"id": "tableviewer",
"name": "Table Viewer",
"description": "Display data as interactive sortable and filterable tables.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300
},
{
"id": "diffviewer",
"name": "Diff Viewer",
"description": "Show code or text changes as a side-by-side diff with syntax highlighting.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300
},
{
"id": "formbuilder",
"name": "Form Builder",
"description": "Collect structured input from users via dynamically generated forms.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300
},
{
"id": "kanban",
"name": "Kanban Board",
"description": "Visualize tasks and progress as a kanban board with priority badges.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300
},
{
"id": "timeline",
"name": "Timeline",
"description": "Render events as an interactive vertical timeline with categories.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300
},
{
"id": "codeplayground",
"name": "Code Playground",
"description": "Display code snippets with syntax highlighting and run JavaScript in a sandbox.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300
}
]
@@ -52,5 +52,71 @@
"type": "builtin",
"env_keys": [],
"bundled": true
},
{
"id": "tableviewer",
"name": "tableviewer",
"display_name": "Table Viewer",
"description": "Display data as interactive sortable and filterable tables.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300,
"bundled": true
},
{
"id": "diffviewer",
"name": "diffviewer",
"display_name": "Diff Viewer",
"description": "Show code or text changes as a side-by-side diff with syntax highlighting.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300,
"bundled": true
},
{
"id": "formbuilder",
"name": "formbuilder",
"display_name": "Form Builder",
"description": "Collect structured input from users via dynamically generated forms.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300,
"bundled": true
},
{
"id": "kanban",
"name": "kanban",
"display_name": "Kanban Board",
"description": "Visualize tasks and progress as a kanban board with priority badges.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300,
"bundled": true
},
{
"id": "timeline",
"name": "timeline",
"display_name": "Timeline",
"description": "Render events as an interactive vertical timeline with categories.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300,
"bundled": true
},
{
"id": "codeplayground",
"name": "codeplayground",
"display_name": "Code Playground",
"description": "Display code snippets with syntax highlighting and run JavaScript in a sandbox.",
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300,
"bundled": true
}
]
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { Button } from '../../../ui/button';
import { SwitchModelModal } from './SwitchModelModal';
import LLMProviderDialog from '../../providers/LLMProviderDialog';
import type { View } from '../../../../utils/navigationUtils';
import { shouldShowPredefinedModels } from '../predefinedModelsUtils';
import { defineMessages, useIntl } from '../../../../i18n';
@@ -23,6 +24,7 @@ interface ConfigureModelButtonsProps {
export default function ModelSettingsButtons({ setView }: ConfigureModelButtonsProps) {
const intl = useIntl();
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
const [isProviderDialogOpen, setIsProviderDialogOpen] = useState(false);
const hasPredefinedModels = shouldShowPredefinedModels();
return (
@@ -35,25 +37,28 @@ export default function ModelSettingsButtons({ setView }: ConfigureModelButtonsP
>
{intl.formatMessage(i18n.switchModels)}
</Button>
{isAddModelModalOpen ? (
{isAddModelModalOpen && (
<SwitchModelModal
sessionId={null}
setView={setView}
onClose={() => setIsAddModelModalOpen(false)}
/>
) : null}
)}
{!hasPredefinedModels && (
<Button
className="flex items-center gap-2 justify-center"
variant="secondary"
size="sm"
onClick={() => {
setView('ConfigureProviders');
}}
onClick={() => setIsProviderDialogOpen(true)}
>
{intl.formatMessage(i18n.configureProviders)}
</Button>
)}
<LLMProviderDialog
open={isProviderDialogOpen}
onClose={() => setIsProviderDialogOpen(false)}
setView={setView}
/>
</div>
);
}
@@ -0,0 +1,42 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/dialog';
import { LLMProviderContent } from './LLMProviderPage';
import { ScrollArea } from '../../ui/scroll-area';
import type { View } from '../../../utils/navigationUtils';
interface LLMProviderDialogProps {
open: boolean;
onClose: () => void;
onProviderLaunched?: (model?: string) => void;
setView?: (view: View) => void;
}
export default function LLMProviderDialog({
open,
onClose,
onProviderLaunched,
setView,
}: LLMProviderDialogProps) {
return (
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
<DialogContent className="sm:max-w-[680px] p-0 gap-0 max-h-[85vh] flex flex-col overflow-hidden">
<DialogHeader className="px-6 pt-6 pb-4 border-b border-border-primary shrink-0">
<DialogTitle className="text-xl font-medium">LLM Provider</DialogTitle>
</DialogHeader>
<ScrollArea className="flex-1 min-h-0">
<div className="px-6 py-5">
<LLMProviderContent
onProviderLaunched={(model) => {
onClose();
onProviderLaunched?.(model);
}}
setView={(view) => {
onClose();
setView?.(view);
}}
/>
</div>
</ScrollArea>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,511 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { ScrollArea } from '../../ui/scroll-area';
import BackButton from '../../ui/BackButton';
import { Button } from '../../ui/button';
import { useConfig } from '../../ConfigContext';
import { useModelAndProvider } from '../../ModelAndProviderContext';
import {
ProviderDetails,
UpdateCustomProviderRequest,
DeclarativeProviderConfig,
checkProvider,
createCustomProvider,
updateCustomProvider,
removeCustomProvider,
getCustomProvider,
} from '../../../api';
import { createNavigationHandler } from '../../../utils/navigationUtils';
import type { View } from '../../../utils/navigationUtils';
import {
Plus,
Wifi,
WifiOff,
Loader2,
CheckCircle2,
Circle,
Settings2,
ChevronRight,
Zap,
} from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/dialog';
import CustomProviderForm from './modal/subcomponents/forms/CustomProviderForm';
import { SwitchModelModal } from '../models/subcomponents/SwitchModelModal';
import ProviderConfigurationModal from './modal/ProviderConfigurationModal';
import ExampleParserForm from './subcomponents/ExampleParserForm';
// ──────────────────────────────────────────────
// ProviderRow
// ──────────────────────────────────────────────
type ConnStatus = 'idle' | 'testing' | 'ok' | 'fail';
function ProviderRow({
provider,
isActive,
onConfigure,
onSelect,
}: {
provider: ProviderDetails;
isActive: boolean;
onConfigure: () => void;
onSelect: () => void;
}) {
const [connStatus, setConnStatus] = useState<ConnStatus>('idle');
const testConnection = useCallback(
async (e: React.MouseEvent) => {
e.stopPropagation();
setConnStatus('testing');
try {
await checkProvider({ body: { provider: provider.name }, throwOnError: true });
setConnStatus('ok');
} catch {
setConnStatus('fail');
}
setTimeout(() => setConnStatus('idle'), 3000);
},
[provider.name]
);
const ConnIcon = () => {
if (connStatus === 'testing') return <Loader2 className="h-4 w-4 animate-spin text-blue-500" />;
if (connStatus === 'ok') return <Wifi className="h-4 w-4 text-green-500" />;
if (connStatus === 'fail') return <WifiOff className="h-4 w-4 text-red-500" />;
return <Wifi className="h-4 w-4 text-text-secondary" />;
};
const connLabel =
connStatus === 'testing' ? 'Testing…' :
connStatus === 'ok' ? 'Connected' :
connStatus === 'fail' ? 'Failed' : 'Test';
return (
<div
className={`flex items-center gap-4 px-4 py-3 rounded-xl border transition-all cursor-pointer group
${isActive
? 'border-blue-500/40 bg-blue-500/5 hover:bg-blue-500/10'
: 'border-border-primary bg-background-secondary hover:bg-background-tertiary'
}`}
onClick={onSelect}
>
<div className="shrink-0">
{isActive ? (
<CheckCircle2 className="h-5 w-5 text-blue-500" />
) : provider.is_configured ? (
<Circle className="h-5 w-5 text-text-secondary fill-text-secondary/20" />
) : (
<Circle className="h-5 w-5 text-text-secondary/40" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm text-text-primary truncate">
{provider.metadata.display_name}
</span>
{isActive && (
<span className="shrink-0 text-xs px-2 py-0.5 rounded-full bg-blue-500/15 text-blue-500 font-medium">
Active
</span>
)}
{!provider.is_configured && (
<span className="shrink-0 text-xs px-2 py-0.5 rounded-full bg-text-secondary/10 text-text-secondary">
Not configured
</span>
)}
</div>
{provider.metadata.description && (
<p className="text-xs text-text-secondary mt-0.5 truncate">
{provider.metadata.description}
</p>
)}
</div>
<div className="flex items-center gap-2 shrink-0" onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="sm"
onClick={testConnection}
disabled={connStatus === 'testing' || !provider.is_configured}
title="Test connectivity"
className="h-8 px-2 gap-1.5 text-xs"
>
<ConnIcon />
<span className="hidden sm:inline">{connLabel}</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={onConfigure}
title="Configure"
className="h-8 px-2"
>
<Settings2 className="h-4 w-4" />
</Button>
</div>
<ChevronRight className="h-4 w-4 text-text-secondary/40 shrink-0 group-hover:text-text-secondary transition-colors" />
</div>
);
}
// ──────────────────────────────────────────────
// AddProviderDialog
// ──────────────────────────────────────────────
function AddProviderDialog({
open,
onClose,
onCreated,
}: {
open: boolean;
onClose: () => void;
onCreated: (providerId: string) => void;
}) {
const [tab, setTab] = useState<'example' | 'manual'>('example');
const [prefill, setPrefill] = useState<UpdateCustomProviderRequest | null>(null);
const handleExampleParsed = useCallback((data: UpdateCustomProviderRequest) => {
setPrefill(data);
setTab('manual');
}, []);
const handleSubmit = useCallback(
async (data: UpdateCustomProviderRequest) => {
const result = await createCustomProvider({ body: data, throwOnError: true });
const id = result.data?.provider_name;
onClose();
setPrefill(null);
setTab('example');
if (id) onCreated(id);
},
[onClose, onCreated]
);
const handleCancel = useCallback(() => {
onClose();
setPrefill(null);
setTab('example');
}, [onClose]);
return (
<Dialog open={open} onOpenChange={handleCancel}>
<DialogContent className="sm:max-w-[620px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Add Provider</DialogTitle>
</DialogHeader>
<div className="flex gap-1 p-1 rounded-lg bg-background-secondary border border-border-primary mb-2">
<button
onClick={() => setTab('example')}
className={`flex-1 text-sm py-1.5 px-3 rounded-md transition-colors ${
tab === 'example'
? 'bg-background-primary text-text-primary shadow-sm'
: 'text-text-secondary hover:text-text-primary'
}`}
>
<Zap className="h-3.5 w-3.5 inline mr-1.5" />
Paste example
</button>
<button
onClick={() => setTab('manual')}
className={`flex-1 text-sm py-1.5 px-3 rounded-md transition-colors ${
tab === 'manual'
? 'bg-background-primary text-text-primary shadow-sm'
: 'text-text-secondary hover:text-text-primary'
}`}
>
<Settings2 className="h-3.5 w-3.5 inline mr-1.5" />
Manual setup
</button>
</div>
{tab === 'example' ? (
<ExampleParserForm onParsed={handleExampleParsed} onCancel={handleCancel} />
) : (
<CustomProviderForm
initialData={prefill}
isEditable={true}
onSubmit={handleSubmit}
onCancel={handleCancel}
/>
)}
</DialogContent>
</Dialog>
);
}
// ──────────────────────────────────────────────
// LLMProviderContent ← pure content, no page chrome
// Used both by LLMProviderPage (full-page route)
// and LLMProviderDialog (dialog embed)
// ──────────────────────────────────────────────
export interface LLMProviderContentProps {
/** Called when the user picks a model — lets the parent close itself */
onProviderLaunched?: (model?: string) => void;
/** Optional override so the Dialog can supply its own setView */
setView?: (view: View) => void;
}
export function LLMProviderContent({ onProviderLaunched, setView: setViewProp }: LLMProviderContentProps) {
const { getProviders } = useConfig();
const navigateHook = useNavigate();
const setView = setViewProp ?? createNavigationHandler(navigateHook);
const { getCurrentModelAndProvider } = useModelAndProvider();
const [loading, setLoading] = useState(true);
const [providers, setProviders] = useState<ProviderDetails[]>([]);
const [activeProvider, setActiveProvider] = useState<string | null>(null);
const [showAdd, setShowAdd] = useState(false);
const [configuringProvider, setConfiguringProvider] = useState<ProviderDetails | null>(null);
const [showCustomEdit, setShowCustomEdit] = useState(false);
const [editingProvider, setEditingProvider] = useState<{
id: string;
config: DeclarativeProviderConfig;
isEditable: boolean;
providerType: string;
} | null>(null);
const [isActiveProvider, setIsActiveProvider] = useState(false);
const [switchModelTarget, setSwitchModelTarget] = useState<string | null>(null);
const initialLoadDone = useRef(false);
const loadAll = useCallback(async () => {
setLoading(true);
try {
const result = await getProviders(!initialLoadDone.current);
if (result) {
setProviders(result);
initialLoadDone.current = true;
}
const { provider } = await getCurrentModelAndProvider();
setActiveProvider(provider);
} catch (err) {
console.error('Failed to load providers', err);
} finally {
setLoading(false);
}
}, [getProviders, getCurrentModelAndProvider]);
useEffect(() => {
loadAll();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const refresh = useCallback(async () => {
const result = await getProviders(true);
if (result) setProviders(result);
const { provider } = await getCurrentModelAndProvider();
setActiveProvider(provider);
}, [getProviders, getCurrentModelAndProvider]);
const handleConfigureProvider = useCallback(
async (provider: ProviderDetails) => {
if (provider.provider_type === 'Custom') {
const result = await getCustomProvider({ path: { id: provider.name }, throwOnError: true });
if (result.data) {
setEditingProvider({
id: provider.name,
config: result.data.config,
isEditable: result.data.is_editable,
providerType: provider.provider_type,
});
try {
const pm = await getCurrentModelAndProvider();
setIsActiveProvider(provider.name === pm.provider);
} catch {
setIsActiveProvider(false);
}
setShowCustomEdit(true);
}
} else {
setConfiguringProvider(provider);
}
},
[getCurrentModelAndProvider]
);
const handleUpdateCustomProvider = useCallback(
async (data: UpdateCustomProviderRequest) => {
if (!editingProvider) return;
await updateCustomProvider({
path: { id: editingProvider.id },
body: data,
throwOnError: true,
});
const id = editingProvider.id;
setShowCustomEdit(false);
setEditingProvider(null);
await refresh();
setSwitchModelTarget(id);
},
[editingProvider, refresh]
);
const handleDeleteCustomProvider = useCallback(async () => {
if (!editingProvider) return;
await removeCustomProvider({ path: { id: editingProvider.id }, throwOnError: true });
setShowCustomEdit(false);
setEditingProvider(null);
setIsActiveProvider(false);
await refresh();
}, [editingProvider, refresh]);
const sortedProviders = [...providers].sort((a, b) => {
if (a.name === activeProvider) return -1;
if (b.name === activeProvider) return 1;
if (a.is_configured !== b.is_configured) return a.is_configured ? -1 : 1;
return a.metadata.display_name.localeCompare(b.metadata.display_name);
});
const editTitle = editingProvider
? editingProvider.isEditable ? 'Edit Provider' : 'Configure Provider'
: 'Add Provider';
const editInitialData = editingProvider
? {
engine: editingProvider.config.engine,
display_name: editingProvider.config.display_name,
api_url: editingProvider.config.base_url,
base_path: editingProvider.config.base_path ?? undefined,
api_key: '',
models: editingProvider.config.models.map((m) => m.name),
supports_streaming: editingProvider.config.supports_streaming ?? true,
requires_auth: editingProvider.config.requires_auth ?? true,
headers: editingProvider.config.headers ?? undefined,
catalog_provider_id: editingProvider.config.catalog_provider_id ?? undefined,
}
: null;
return (
<>
{/* Header row: title + add button */}
<div className="flex items-center justify-between mb-5">
<p className="text-sm text-text-secondary">
Active provider is used globally. Click a row to switch.
</p>
<Button onClick={() => setShowAdd(true)} className="gap-1.5 shrink-0" size="sm">
<Plus className="h-4 w-4" />
Add Provider
</Button>
</div>
{/* Provider list */}
<div className="space-y-2">
{loading ? (
<div className="flex items-center justify-center py-12 text-text-secondary">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Loading providers
</div>
) : sortedProviders.length === 0 ? (
<div className="text-center py-12 text-text-secondary text-sm">
No providers found.{' '}
<button className="text-blue-500 hover:underline" onClick={() => setShowAdd(true)}>
Add one
</button>
.
</div>
) : (
sortedProviders.map((provider) => (
<ProviderRow
key={provider.name}
provider={provider}
isActive={provider.name === activeProvider}
onConfigure={() => handleConfigureProvider(provider)}
onSelect={() => {
if (provider.is_configured) {
setSwitchModelTarget(provider.name);
} else {
handleConfigureProvider(provider);
}
}}
/>
))
)}
</div>
{/* Add provider dialog */}
<AddProviderDialog
open={showAdd}
onClose={() => setShowAdd(false)}
onCreated={async (id) => { await refresh(); setSwitchModelTarget(id); }}
/>
{/* Edit custom provider dialog */}
<Dialog
open={showCustomEdit}
onOpenChange={() => { setShowCustomEdit(false); setEditingProvider(null); }}
>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editTitle}</DialogTitle>
</DialogHeader>
<CustomProviderForm
initialData={editInitialData}
isEditable={editingProvider?.isEditable ?? true}
onSubmit={handleUpdateCustomProvider}
onCancel={() => { setShowCustomEdit(false); setEditingProvider(null); }}
onDelete={editingProvider?.providerType === 'Custom' ? handleDeleteCustomProvider : undefined}
isActiveProvider={isActiveProvider}
/>
</DialogContent>
</Dialog>
{/* Configure builtin provider */}
{configuringProvider && (
<ProviderConfigurationModal
provider={configuringProvider}
onClose={() => { setConfiguringProvider(null); refresh(); }}
onConfigured={async (provider) => {
setConfiguringProvider(null);
await refresh();
setSwitchModelTarget(provider.name);
}}
/>
)}
{/* Switch model after provider selection */}
{switchModelTarget && (
<SwitchModelModal
sessionId={null}
onClose={() => setSwitchModelTarget(null)}
setView={(view) => { setSwitchModelTarget(null); setView(view); }}
onModelSelected={onProviderLaunched}
initialProvider={switchModelTarget}
titleOverride="Choose Model"
/>
)}
</>
);
}
// ──────────────────────────────────────────────
// LLMProviderPage ← full-page route wrapper
// ──────────────────────────────────────────────
export default function LLMProviderPage({
onClose,
onProviderLaunched,
}: {
onClose: () => void;
onProviderLaunched?: (model?: string) => void;
}) {
const navigate = useNavigate();
const setView = useCallback(createNavigationHandler(navigate), [navigate]);
return (
<div className="h-screen w-full flex flex-col bg-background-primary text-text-primary">
<ScrollArea className="flex-1 w-full">
<div className="w-full max-w-3xl mx-auto px-6 pt-12 pb-10">
<div className="flex items-center mb-1 no-drag">
<BackButton onClick={onClose} />
</div>
<h1 className="text-3xl font-light pt-5 mb-6">LLM Provider</h1>
<LLMProviderContent onProviderLaunched={onProviderLaunched} setView={setView} />
</div>
</ScrollArea>
</div>
);
}
@@ -0,0 +1,228 @@
import { useState, useCallback } from 'react';
import { Button } from '../../../ui/button';
import { UpdateCustomProviderRequest } from '../../../../api';
import { Sparkles, AlertCircle } from 'lucide-react';
interface ParsedResult {
engine: string;
display_name: string;
api_url: string;
api_key: string;
models: string[];
requires_auth: boolean;
supports_streaming: boolean;
}
function parseExample(raw: string): ParsedResult | null {
const text = raw.trim();
if (!text) return null;
const result: ParsedResult = {
engine: 'openai_compatible',
display_name: '',
api_url: '',
api_key: '',
models: [],
requires_auth: true,
supports_streaming: true,
};
// ── Extract URL ──
// curl URL (first positional arg after 'curl')
const curlUrlMatch = text.match(/curl\s+(?:-[^\s]+\s+\S+\s+)*['"](https?:\/\/[^'"]+)['"]/);
const bareUrlMatch = text.match(/curl\s+['"]?(https?:\/\/[^\s'"]+)['"]?/);
const pythonBaseUrl = text.match(/base_url\s*=\s*['"]([^'"]+)['"]/);
const jsBaseUrl = text.match(/baseURL\s*[:=]\s*['"`]([^'"`]+)['"`]/);
const genericUrl = text.match(/https?:\/\/[^\s'"`,)]+/);
const rawUrl =
(curlUrlMatch?.[1] || bareUrlMatch?.[1] || pythonBaseUrl?.[1] || jsBaseUrl?.[1] || genericUrl?.[0]) ?? '';
if (rawUrl) {
try {
const u = new URL(rawUrl);
// Strip known chat-completion paths, keep base
const cleanPath = u.pathname
.replace(/\/(v\d+\/)?(chat\/completions|completions|messages)\/?$/, '')
.replace(/\/$/, '');
result.api_url = `${u.protocol}//${u.host}${cleanPath}`;
// Derive display name from hostname
const hostParts = u.hostname.replace(/^api\./, '').split('.');
result.display_name = hostParts[0].charAt(0).toUpperCase() + hostParts[0].slice(1);
} catch {
result.api_url = rawUrl;
}
}
// ── Extract API key ──
const bearerMatch = text.match(/Bearer\s+([A-Za-z0-9\-_.~+/]+=*)/);
const pythonApiKey = text.match(/api_key\s*=\s*['"]([^'"]+)['"]/);
const jsApiKey = text.match(/apiKey\s*[:=]\s*['"`]([^'"`]+)['"`]/);
const envKey = text.match(/process\.env\.([A-Z_]+)/);
if (bearerMatch) result.api_key = bearerMatch[1];
else if (pythonApiKey) result.api_key = pythonApiKey[1];
else if (jsApiKey) result.api_key = jsApiKey[1];
else if (envKey) result.api_key = `$${envKey[1]}`;
result.requires_auth = !!result.api_key || text.includes('api_key') || text.includes('Bearer');
// ── Extract model(s) ──
const modelInJson = text.match(/"model"\s*:\s*"([^"]+)"/);
const modelPython = text.match(/model\s*=\s*['"]([^'"]+)['"]/g);
const modelJs = text.match(/model\s*:\s*['"`]([^'"`]+)['"`]/);
if (modelInJson) {
result.models = [modelInJson[1]];
} else if (modelPython) {
// deduplicate
const names = modelPython
.map((m) => m.match(/['"]([^'"]+)['"]/)?.[1])
.filter((m): m is string => !!m && !m.includes(' '));
result.models = [...new Set(names)];
} else if (modelJs) {
result.models = [modelJs[1]];
}
// ── Detect engine ──
const lower = text.toLowerCase();
if (lower.includes('anthropic') || lower.includes('claude')) {
result.engine = 'anthropic_compatible';
if (!result.display_name) result.display_name = 'Anthropic';
} else if (lower.includes('ollama')) {
result.engine = 'ollama_compatible';
result.requires_auth = false;
result.api_key = '';
if (!result.display_name) result.display_name = 'Ollama';
} else {
result.engine = 'openai_compatible';
}
// ── Streaming hint ──
if (lower.includes('stream') && (lower.includes('false') || lower.includes('false'))) {
result.supports_streaming = false;
}
if (!result.api_url && result.models.length === 0) return null;
return result;
}
interface ExampleParserFormProps {
onParsed: (data: UpdateCustomProviderRequest) => void;
onCancel: () => void;
}
const PLACEHOLDERS = [
`curl https://api.openai.com/v1/chat/completions \\
-H "Authorization: Bearer sk-proj-…" \\
-d '{"model": "gpt-4o", "messages": [...]}'`,
`from openai import OpenAI
client = OpenAI(base_url="https://api.deepseek.com", api_key="sk-…")
client.chat.completions.create(model="deepseek-chat", messages=[...])`,
];
export default function ExampleParserForm({ onParsed, onCancel }: ExampleParserFormProps) {
const [text, setText] = useState('');
const [error, setError] = useState('');
const [parsed, setParsed] = useState<ParsedResult | null>(null);
const [placeholder] = useState(() => PLACEHOLDERS[Math.floor(Math.random() * PLACEHOLDERS.length)]);
const handleParse = useCallback(() => {
setError('');
const result = parseExample(text);
if (!result) {
setError("Couldn't detect provider details. Check the example format or use Manual setup.");
return;
}
setParsed(result);
}, [text]);
const handleUse = useCallback(() => {
if (!parsed) return;
onParsed({
engine: parsed.engine,
display_name: parsed.display_name,
api_url: parsed.api_url,
api_key: parsed.api_key,
models: parsed.models,
requires_auth: parsed.requires_auth,
supports_streaming: parsed.supports_streaming,
headers: {},
});
}, [parsed, onParsed]);
return (
<div className="mt-2 space-y-4">
<p className="text-sm text-text-secondary">
Paste a curl command, Python snippet, or JS code that calls your provider. We'll extract the
URL, API key, and model automatically.
</p>
<textarea
value={text}
onChange={(e) => { setText(e.target.value); setParsed(null); setError(''); }}
rows={8}
placeholder={placeholder}
className="w-full rounded-lg border border-border-primary bg-background-secondary text-text-primary text-sm font-mono px-3 py-2.5 resize-none focus:outline-none focus:ring-2 focus:ring-blue-500/30 placeholder:text-text-secondary/40"
/>
{error && (
<div className="flex items-start gap-2 text-sm text-red-500 bg-red-500/10 px-3 py-2 rounded-lg">
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
{error}
</div>
)}
{parsed && (
<div className="rounded-lg border border-green-500/30 bg-green-500/5 px-4 py-3 space-y-1.5">
<p className="text-xs font-semibold text-green-600 uppercase tracking-wide mb-2">Detected</p>
<Row label="Provider" value={parsed.display_name || ''} />
<Row label="API URL" value={parsed.api_url || ''} />
<Row label="Engine" value={parsed.engine.replace('_compatible', '')} />
<Row
label="API Key"
value={
!parsed.requires_auth
? 'None (no auth)'
: parsed.api_key
? parsed.api_key.length > 20
? parsed.api_key.slice(0, 8) + '' + parsed.api_key.slice(-4)
: parsed.api_key
: 'Not detected (fill in form)'
}
/>
<Row
label="Models"
value={parsed.models.length > 0 ? parsed.models.join(', ') : 'Not detected (fill in form)'}
/>
</div>
)}
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
{parsed ? (
<Button type="button" onClick={handleUse}>
Continue with these settings
</Button>
) : (
<Button type="button" onClick={handleParse} disabled={!text.trim()} className="gap-2">
<Sparkles className="h-4 w-4" />
Parse
</Button>
)}
</div>
</div>
);
}
function Row({ label, value }: { label: string; value: string }) {
return (
<div className="flex gap-2 text-sm">
<span className="w-20 text-text-secondary shrink-0">{label}</span>
<span className="text-text-primary font-mono text-xs break-all">{value}</span>
</div>
);
}