mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-22 03:07:02 +00:00
feat: add native LLaMA.cpp local provider support (#1346)
Co-authored-by: bytecii <bytecii@users.noreply.github.com> Co-authored-by: bytecii <994513625@qq.com>
This commit is contained in:
parent
478926d33f
commit
d606fae458
7 changed files with 499 additions and 262 deletions
|
|
@ -20,6 +20,7 @@ PLATFORM_ALIAS_MAPPING: Final[dict[str, str]] = {
|
|||
"z.ai": "openai-compatible-model",
|
||||
"ModelArk": "openai-compatible-model",
|
||||
"grok": "openai-compatible-model",
|
||||
"llama.cpp": "openai-compatible-model",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ def test_normalize_model_platform_maps_known_aliases():
|
|||
assert normalize_model_platform("grok") == "openai-compatible-model"
|
||||
assert normalize_model_platform("z.ai") == "openai-compatible-model"
|
||||
assert normalize_model_platform("ModelArk") == "openai-compatible-model"
|
||||
assert normalize_model_platform("llama.cpp") == "openai-compatible-model"
|
||||
|
||||
|
||||
def test_normalize_model_platform_keeps_non_alias_unchanged():
|
||||
|
|
@ -43,7 +44,7 @@ def test_normalized_model_platform_type_applies_in_pydantic_model():
|
|||
optional_model_platform: NormalizedOptionalModelPlatform = None
|
||||
|
||||
item = _Model(
|
||||
model_platform="grok",
|
||||
model_platform="llama.cpp",
|
||||
optional_model_platform="ModelArk",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ print(f"Server started on http://localhost:{port}")
|
|||
ollama pull qwen2.5:7b
|
||||
```
|
||||
|
||||
```bash
|
||||
# LLaMA.cpp server https://github.com/ggml-org/llama.cpp/tree/master/tools/server
|
||||
./llama-server -m /path/to/model.gguf --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
2. Setting your model
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ Eigent can run in two modes. Your choice here affects how you are billed and wha
|
|||
- **Cloud Version:** We provide pre-configured, state-of-the-art models, including GPT-4.1, GPT-4.1 mini and Gemini 2.5 Pro. Using these models is the easiest way to get started and will be billed to your account based on usage (credits).
|
||||
- **Self-hosted Version:** You can connect your own models.
|
||||
- **Cloud Models:** Connect your personal accounts from providers like OpenAI, Anthropic, Qwen, Deepseek and Azure by entering your own API key.
|
||||
- **Local Models:** For advanced users, you can run models locally using Ollama, vLLM, or SGLang.
|
||||
- **Local Models:** For advanced users, you can run models locally using Ollama, vLLM, SGLang, LM Studio, or LLaMA.cpp server.
|
||||
|
||||
### **MCP Servers**
|
||||
|
||||
|
|
|
|||
75
src/assets/model/llamacpp.svg
Normal file
75
src/assets/model/llamacpp.svg
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="Layer_1"
|
||||
version="1.1"
|
||||
viewBox="0 0 250 250"
|
||||
sodipodi:docname="llama1-icon-transparent.svg"
|
||||
width="250"
|
||||
height="250"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="2.48"
|
||||
inkscape:cx="49.596774"
|
||||
inkscape:cy="189.91935"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1440"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" />
|
||||
<!-- Generator: Adobe Illustrator 29.3.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 151) -->
|
||||
<defs
|
||||
id="defs1">
|
||||
<style
|
||||
id="style1">
|
||||
.st0 {
|
||||
fill: #ff8236;
|
||||
}
|
||||
.st1 {
|
||||
fill: #fff;
|
||||
}
|
||||
.st2 {
|
||||
fill: #1b1f20;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g
|
||||
id="g7">
|
||||
<g
|
||||
id="g6"
|
||||
transform="translate(-995.51066,-129.70875)">
|
||||
<path
|
||||
class="st0"
|
||||
d="m 1163.3,226.8 -13.5,24 c -17.8,-13.7 -44.2,-15.7 -62,-1 -28.7,23.7 -26.7,78.5 18,78.8 12.5,0 23.1,-5.9 34.5,-9.8 l 6,23.9 c -10.1,4.7 -20.4,9.5 -31.5,11 -101.2,13.8 -95.4,-132.3 -3.9,-139.9 19.2,-1.6 36.1,3.4 52.5,13 z"
|
||||
id="path4" />
|
||||
<path
|
||||
class="st0"
|
||||
d="m 1093.4,203.8 c -15.4,4.6 -29.7,13.1 -40.5,25 -2,-24.2 3.4,-73.1 30.3,-82.7 4,-1.4 17.7,-4.9 17.3,2.2 -0.4,7.1 -9.9,19.3 -12.2,25.9 -4,11.6 -0.3,19.6 5.2,29.7 z"
|
||||
id="path5" />
|
||||
<polygon
|
||||
class="st0"
|
||||
points="1131.4,307.8 1116.4,307.8 1116.4,290.8 1099.4,290.8 1099.4,276.8 1114.9,276.8 1116.4,275.3 1116.4,258.8 1131.4,258.8 1131.4,276.8 1147.4,276.8 1147.4,290.8 1131.4,290.8 "
|
||||
id="polygon5" />
|
||||
<polygon
|
||||
class="st0"
|
||||
points="1186.4,290.8 1186.4,307.8 1171.4,307.8 1171.4,290.8 1155.4,290.8 1155.4,276.8 1171.4,276.8 1171.4,258.8 1186.4,258.8 1186.4,275.3 1187.9,276.8 1203.4,276.8 1203.4,290.8 "
|
||||
id="polygon6" />
|
||||
<path
|
||||
class="st0"
|
||||
d="m 1142.3,156.9 c 2,3 -9.3,15.9 -11.1,19.2 -5.2,9.8 -1.7,15.4 2.2,24.7 -11.3,-1.7 -21.8,-0.3 -33,1 2.5,-21.5 14.6,-52.8 41.9,-44.9 z"
|
||||
id="path6" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -53,7 +53,7 @@ import {
|
|||
Server,
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
|
@ -65,6 +65,7 @@ import bedrockImage from '@/assets/model/bedrock.svg';
|
|||
import deepseekImage from '@/assets/model/deepseek.svg';
|
||||
import eigentImage from '@/assets/model/eigent.svg';
|
||||
import geminiImage from '@/assets/model/gemini.svg';
|
||||
import llamaCppImage from '@/assets/model/llamacpp.svg';
|
||||
import lmstudioImage from '@/assets/model/lmstudio.svg';
|
||||
import minimaxImage from '@/assets/model/minimax.svg';
|
||||
import modelarkImage from '@/assets/model/modelark.svg';
|
||||
|
|
@ -77,11 +78,23 @@ import sglangImage from '@/assets/model/sglang.svg';
|
|||
import vllmImage from '@/assets/model/vllm.svg';
|
||||
import zaiImage from '@/assets/model/zai.svg';
|
||||
|
||||
const LOCAL_PROVIDER_NAMES = ['ollama', 'vllm', 'sglang', 'lmstudio'];
|
||||
const DEFAULT_OLLAMA_ENDPOINT = 'http://localhost:11434/v1';
|
||||
const OLLAMA_ENDPOINT_AUTO_FIX_TITLE = 'Ollama endpoint updated';
|
||||
const OLLAMA_ENDPOINT_AUTO_FIX_DESC =
|
||||
'Added /v1 once. You can remove it if not needed.';
|
||||
import {
|
||||
appendV1ToEndpoint,
|
||||
canAutoFixOllamaEndpoint,
|
||||
DARK_FILL_MODELS,
|
||||
getDefaultLocalEndpoint,
|
||||
getLocalPlatformName,
|
||||
LLAMA_CPP_PROVIDER_ID,
|
||||
LMSTUDIO_PROVIDER_ID,
|
||||
LOCAL_MODEL_OPTIONS,
|
||||
OLLAMA_ENDPOINT_AUTO_FIX_DESC,
|
||||
OLLAMA_ENDPOINT_AUTO_FIX_TITLE,
|
||||
OLLAMA_PROVIDER_ID,
|
||||
PROVIDER_AVATAR_URLS,
|
||||
SGLANG_PROVIDER_ID,
|
||||
toEndpointBaseUrl,
|
||||
VLLM_PROVIDER_ID,
|
||||
} from './localModels';
|
||||
|
||||
// Sidebar tab types
|
||||
type SidebarTab =
|
||||
|
|
@ -92,25 +105,8 @@ type SidebarTab =
|
|||
| 'local-ollama'
|
||||
| 'local-vllm'
|
||||
| 'local-sglang'
|
||||
| 'local-lmstudio';
|
||||
|
||||
// Provider logos that use dark fills (black or currentColor) and need inversion in dark mode
|
||||
const DARK_FILL_MODELS = new Set([
|
||||
'openai',
|
||||
'anthropic',
|
||||
'moonshot',
|
||||
'ollama',
|
||||
'openrouter',
|
||||
'lmstudio',
|
||||
'z.ai',
|
||||
'openai-compatible-model',
|
||||
]);
|
||||
|
||||
const PROVIDER_AVATAR_URLS: Record<string, string> = {
|
||||
'samba-nova': 'https://github.com/sambanova.png',
|
||||
mistral: 'https://github.com/mistralai.png',
|
||||
grok: 'https://github.com/xai-org.png',
|
||||
};
|
||||
| 'local-lmstudio'
|
||||
| 'local-llama.cpp';
|
||||
|
||||
export default function SettingModels() {
|
||||
const {
|
||||
|
|
@ -177,7 +173,8 @@ export default function SettingModels() {
|
|||
|
||||
// Local Model independent state - per platform
|
||||
const [localEnabled, setLocalEnabled] = useState(true);
|
||||
const [localPlatform, setLocalPlatform] = useState('ollama');
|
||||
const [localPlatform, setLocalPlatform] =
|
||||
useState<string>(OLLAMA_PROVIDER_ID);
|
||||
const [localEndpoints, setLocalEndpoints] = useState<Record<string, string>>(
|
||||
{}
|
||||
);
|
||||
|
|
@ -190,36 +187,72 @@ export default function SettingModels() {
|
|||
const [localInputError, setLocalInputError] = useState(false);
|
||||
const [localPrefer, setLocalPrefer] = useState(false); // Local model prefer state (for current platform)
|
||||
|
||||
// Ollama-specific state for model fetching
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([]);
|
||||
const [ollamaModelsLoading, setOllamaModelsLoading] = useState(false);
|
||||
const [ollamaModelsError, setOllamaModelsError] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
// Per-platform model list state: { models, loading, error } keyed by platform ID.
|
||||
const [platformModelState, setPlatformModelState] = useState<
|
||||
Record<string, { models: string[]; loading: boolean; error: string | null }>
|
||||
>({});
|
||||
const [ollamaEndpointAutoFixedOnce, setOllamaEndpointAutoFixedOnce] =
|
||||
useState(false);
|
||||
|
||||
// Fetch available models from Ollama API
|
||||
const fetchOllamaModels = async (endpoint?: string) => {
|
||||
const url = endpoint || DEFAULT_OLLAMA_ENDPOINT;
|
||||
setOllamaModelsLoading(true);
|
||||
setOllamaModelsError(null);
|
||||
try {
|
||||
const baseUrl = url.replace(/\/v1\/?$/, '').replace(/\/$/, '');
|
||||
const response = await fetch(`${baseUrl}/api/tags`);
|
||||
if (!response.ok) throw new Error(`Failed: ${response.status}`);
|
||||
// Generic model fetcher driven by LOCAL_MODEL_OPTIONS config.
|
||||
// Only fetches for providers that define fetchPath and parseModels.
|
||||
const fetchModelsForPlatform = useCallback(
|
||||
async (platform: string, endpoint?: string) => {
|
||||
const option = LOCAL_MODEL_OPTIONS.find((m) => m.id === platform);
|
||||
if (!option?.fetchPath || !option?.parseModels) return;
|
||||
|
||||
const data = await response.json();
|
||||
const modelNames = data.models?.map((m: any) => m.name) || [];
|
||||
setOllamaModels(modelNames);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to fetch Ollama models:', error);
|
||||
setOllamaModels([]);
|
||||
setOllamaModelsError('Failed to fetch Ollama models. Is Ollama running?');
|
||||
} finally {
|
||||
setOllamaModelsLoading(false);
|
||||
const url = endpoint || option.defaultEndpoint;
|
||||
setPlatformModelState((prev) => ({
|
||||
...prev,
|
||||
[platform]: {
|
||||
models: prev[platform]?.models || [],
|
||||
loading: true,
|
||||
error: null,
|
||||
},
|
||||
}));
|
||||
try {
|
||||
const baseUrl = toEndpointBaseUrl(url);
|
||||
const response = await fetch(`${baseUrl}${option.fetchPath}`);
|
||||
if (!response.ok) throw new Error(`Failed: ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
const modelNames = option.parseModels(data);
|
||||
setPlatformModelState((prev) => ({
|
||||
...prev,
|
||||
[platform]: { models: modelNames, loading: false, error: null },
|
||||
}));
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to fetch ${option.name} models:`, error);
|
||||
setPlatformModelState((prev) => ({
|
||||
...prev,
|
||||
[platform]: {
|
||||
models: [],
|
||||
loading: false,
|
||||
error: `Failed to fetch ${option.name} models. Is ${option.name} running?`,
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const clearPlatformModelsError = useCallback((platform: string) => {
|
||||
setPlatformModelState((prev) => {
|
||||
const current = prev[platform];
|
||||
if (!current || !current.error) return prev;
|
||||
return { ...prev, [platform]: { ...current, error: null } };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const checkLlamaCppHealth = useCallback(async (endpoint: string) => {
|
||||
const baseUrl = toEndpointBaseUrl(endpoint);
|
||||
const response = await fetch(`${baseUrl}/v1/health`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
'LLaMA.cpp health check failed. Please confirm llama-server is running and reachable.'
|
||||
);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Default model dropdown state (removed - using DropdownMenu's built-in state)
|
||||
|
||||
|
|
@ -270,7 +303,7 @@ export default function SettingModels() {
|
|||
);
|
||||
// Handle local models - load all local providers per platform
|
||||
const localProviders = providerList.filter((p: any) =>
|
||||
LOCAL_PROVIDER_NAMES.includes(p.provider_name)
|
||||
LOCAL_MODEL_OPTIONS.some((model) => model.id === p.provider_name)
|
||||
);
|
||||
|
||||
const endpoints: Record<string, string> = {};
|
||||
|
|
@ -280,10 +313,9 @@ export default function SettingModels() {
|
|||
localProviders.forEach((local: any) => {
|
||||
const platform =
|
||||
local.encrypted_config?.model_platform || local.provider_name;
|
||||
// Auto-populate default Ollama endpoint if not set
|
||||
// Auto-populate platform default endpoint if not set
|
||||
endpoints[platform] =
|
||||
local.endpoint_url ||
|
||||
(platform === 'ollama' ? DEFAULT_OLLAMA_ENDPOINT : '');
|
||||
local.endpoint_url || getDefaultLocalEndpoint(platform);
|
||||
types[platform] = local.encrypted_config?.model_type || '';
|
||||
providerIds[platform] = local.id;
|
||||
|
||||
|
|
@ -298,17 +330,18 @@ export default function SettingModels() {
|
|||
setLocalTypes(types);
|
||||
setLocalProviderIds(providerIds);
|
||||
|
||||
// Fetch Ollama models if ollama endpoint is set
|
||||
const ollamaEndpoint = endpoints['ollama'] || DEFAULT_OLLAMA_ENDPOINT;
|
||||
fetchOllamaModels(ollamaEndpoint);
|
||||
// Fetch model lists for all providers that support it
|
||||
LOCAL_MODEL_OPTIONS.filter((m) => m.fetchPath).forEach((m) => {
|
||||
const ep = endpoints[m.id] || m.defaultEndpoint;
|
||||
fetchModelsForPlatform(m.id, ep);
|
||||
});
|
||||
|
||||
// If no local providers found, initialize empty state with Ollama default
|
||||
if (localProviders.length === 0) {
|
||||
LOCAL_PROVIDER_NAMES.forEach((platform) => {
|
||||
endpoints[platform] =
|
||||
platform === 'ollama' ? DEFAULT_OLLAMA_ENDPOINT : '';
|
||||
types[platform] = '';
|
||||
providerIds[platform] = undefined;
|
||||
LOCAL_MODEL_OPTIONS.forEach((model) => {
|
||||
endpoints[model.id] = getDefaultLocalEndpoint(model.id);
|
||||
types[model.id] = '';
|
||||
providerIds[model.id] = undefined;
|
||||
});
|
||||
setLocalEndpoints(endpoints);
|
||||
setLocalTypes(types);
|
||||
|
|
@ -337,7 +370,7 @@ export default function SettingModels() {
|
|||
fetchSubscription();
|
||||
updateCredits();
|
||||
}
|
||||
}, [items, modelType]);
|
||||
}, [items, modelType, fetchModelsForPlatform]);
|
||||
|
||||
// Get current default model display text
|
||||
const getDefaultModelDisplayText = (): string => {
|
||||
|
|
@ -363,16 +396,7 @@ export default function SettingModels() {
|
|||
|
||||
// Check for local model preference
|
||||
if (localPrefer && localPlatform) {
|
||||
const localModel = localModelOptions.find((m) => m.id === localPlatform);
|
||||
const platformName = localModel
|
||||
? localModel.name
|
||||
: localPlatform === 'ollama'
|
||||
? 'Ollama'
|
||||
: localPlatform === 'vllm'
|
||||
? 'vLLM'
|
||||
: localPlatform === 'sglang'
|
||||
? 'SGLang'
|
||||
: 'LM Studio';
|
||||
const platformName = getLocalPlatformName(localPlatform);
|
||||
const modelType = localTypes[localPlatform] || '';
|
||||
return `${t('setting.local-model')} / ${platformName}${modelType ? ` (${modelType})` : ''}`;
|
||||
}
|
||||
|
|
@ -473,14 +497,6 @@ export default function SettingModels() {
|
|||
{ id: 'minimax_m2_5', name: 'Minimax M2.5' },
|
||||
];
|
||||
|
||||
// Local model options
|
||||
const localModelOptions = [
|
||||
{ id: 'ollama', name: 'Ollama' },
|
||||
{ id: 'vllm', name: 'vLLM' },
|
||||
{ id: 'sglang', name: 'SGLang' },
|
||||
{ id: 'lmstudio', name: 'LM Studio' },
|
||||
];
|
||||
|
||||
const handleVerify = async (idx: number) => {
|
||||
const { apiKey, apiHost, externalConfig, model_type, provider_id } =
|
||||
form[idx];
|
||||
|
|
@ -631,43 +647,6 @@ export default function SettingModels() {
|
|||
}
|
||||
};
|
||||
|
||||
// Local Model verification
|
||||
const isOllamaEndpointMissingV1 = (endpoint: string): boolean => {
|
||||
const trimmed = endpoint.trim();
|
||||
if (!trimmed) return false;
|
||||
try {
|
||||
const normalizedPath = new URL(trimmed).pathname.replace(/\/+$/, '');
|
||||
return !normalizedPath.endsWith('/v1');
|
||||
} catch {
|
||||
return !trimmed.replace(/\/+$/, '').endsWith('/v1');
|
||||
}
|
||||
};
|
||||
const canAutoFixOllamaEndpoint = (endpoint: string): boolean => {
|
||||
const trimmed = endpoint.trim();
|
||||
if (!trimmed || !isOllamaEndpointMissingV1(trimmed)) return false;
|
||||
try {
|
||||
// Auto-fix only when endpoint has no extra path, e.g. http://localhost:11434
|
||||
const normalizedPath = new URL(trimmed).pathname.replace(/\/+$/, '');
|
||||
return normalizedPath === '';
|
||||
} catch {
|
||||
const withoutQueryOrHash = trimmed.split(/[?#]/)[0] || '';
|
||||
const normalized = withoutQueryOrHash.replace(/\/+$/, '');
|
||||
return !normalized.includes('/');
|
||||
}
|
||||
};
|
||||
const appendV1ToEndpoint = (endpoint: string): string => {
|
||||
const trimmed = endpoint.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
const normalizedPath = parsed.pathname.replace(/\/+$/, '');
|
||||
parsed.pathname = `${normalizedPath}/v1`.replace(/\/{2,}/g, '/');
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return `${trimmed.replace(/\/+$/, '')}/v1`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLocalVerify = async () => {
|
||||
setLocalVerifying(true);
|
||||
setLocalError(null);
|
||||
|
|
@ -678,7 +657,7 @@ export default function SettingModels() {
|
|||
// Fallback guard for fast save interactions: ensure one-time auto-fix
|
||||
// still applies even if blur state hasn't committed yet.
|
||||
if (
|
||||
localPlatform === 'ollama' &&
|
||||
localPlatform === OLLAMA_PROVIDER_ID &&
|
||||
!ollamaEndpointAutoFixedOnce &&
|
||||
canAutoFixOllamaEndpoint(currentEndpoint)
|
||||
) {
|
||||
|
|
@ -704,6 +683,10 @@ export default function SettingModels() {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
if (localPlatform === LLAMA_CPP_PROVIDER_ID) {
|
||||
await checkLlamaCppHealth(currentEndpoint);
|
||||
}
|
||||
|
||||
// // 1. Check if endpoint returns response
|
||||
// let baseUrl = localEndpoint;
|
||||
// let testUrl = baseUrl;
|
||||
|
|
@ -740,25 +723,43 @@ export default function SettingModels() {
|
|||
// throw new Error("Endpoint is not responding");
|
||||
// }
|
||||
|
||||
try {
|
||||
const res = await fetchPost('/model/validate', {
|
||||
model_platform: localPlatform,
|
||||
model_type: currentType,
|
||||
api_key: 'not-required',
|
||||
url: currentEndpoint,
|
||||
});
|
||||
if (res.is_tool_calls && res.is_valid) {
|
||||
console.log('success');
|
||||
toast(t('setting.validate-success'), {
|
||||
description: t(
|
||||
'setting.the-model-has-been-verified-to-support-function-calling-which-is-required-to-use-eigent'
|
||||
),
|
||||
closeButton: true,
|
||||
// Temporary: skip /model/validate for llama.cpp.
|
||||
// Current validation flow is not fully compatible.
|
||||
if (localPlatform !== LLAMA_CPP_PROVIDER_ID) {
|
||||
try {
|
||||
const res = await fetchPost('/model/validate', {
|
||||
model_platform: localPlatform,
|
||||
model_type: currentType,
|
||||
api_key: 'not-required',
|
||||
url: currentEndpoint,
|
||||
});
|
||||
} else {
|
||||
console.log('failed', res.message);
|
||||
if (res.is_tool_calls && res.is_valid) {
|
||||
console.log('success');
|
||||
toast(t('setting.validate-success'), {
|
||||
description: t(
|
||||
'setting.the-model-has-been-verified-to-support-function-calling-which-is-required-to-use-eigent'
|
||||
),
|
||||
closeButton: true,
|
||||
});
|
||||
} else {
|
||||
console.log('failed', res.message);
|
||||
const toastId = toast(t('setting.validate-failed'), {
|
||||
description: getValidateMessage(res),
|
||||
action: {
|
||||
label: t('setting.close'),
|
||||
onClick: () => {
|
||||
toast.dismiss(toastId);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
console.log(res);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
const toastId = toast(t('setting.validate-failed'), {
|
||||
description: getValidateMessage(res),
|
||||
description: getValidateMessage(e),
|
||||
action: {
|
||||
label: t('setting.close'),
|
||||
onClick: () => {
|
||||
|
|
@ -766,24 +767,8 @@ export default function SettingModels() {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
console.log(res);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
const toastId = toast(t('setting.validate-failed'), {
|
||||
description: getValidateMessage(e),
|
||||
action: {
|
||||
label: t('setting.close'),
|
||||
onClick: () => {
|
||||
toast.dismiss(toastId);
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
|
||||
// 2. Save to /api/provider/ (save only base URL)
|
||||
|
|
@ -831,6 +816,8 @@ export default function SettingModels() {
|
|||
await handleLocalSwitch(true, local.id);
|
||||
}
|
||||
}
|
||||
|
||||
await fetchModelsForPlatform(localPlatform, currentEndpoint);
|
||||
} catch (e: any) {
|
||||
setLocalError(
|
||||
e.message || t('setting.verification-failed-please-check-endpoint-url')
|
||||
|
|
@ -939,9 +926,8 @@ export default function SettingModels() {
|
|||
if (currentProviderId !== undefined) {
|
||||
await proxyFetchDelete(`/api/provider/${currentProviderId}`);
|
||||
}
|
||||
// Set endpoint to default for Ollama, empty for others
|
||||
const defaultEndpoint =
|
||||
localPlatform === 'ollama' ? DEFAULT_OLLAMA_ENDPOINT : '';
|
||||
// Set endpoint to platform default
|
||||
const defaultEndpoint = getDefaultLocalEndpoint(localPlatform);
|
||||
setLocalEndpoints((prev) => ({
|
||||
...prev,
|
||||
[localPlatform]: defaultEndpoint,
|
||||
|
|
@ -954,12 +940,12 @@ export default function SettingModels() {
|
|||
}
|
||||
setLocalEnabled(true);
|
||||
setActiveModelIdx(null);
|
||||
// Re-fetch Ollama models after reset
|
||||
if (localPlatform === 'ollama') {
|
||||
// Re-fetch model list after reset
|
||||
if (localPlatform === OLLAMA_PROVIDER_ID) {
|
||||
setOllamaEndpointAutoFixedOnce(false);
|
||||
setOllamaModelsError(null);
|
||||
fetchOllamaModels(DEFAULT_OLLAMA_ENDPOINT);
|
||||
}
|
||||
clearPlatformModelsError(localPlatform);
|
||||
await fetchModelsForPlatform(localPlatform);
|
||||
toast.success(t('setting.reset-success'));
|
||||
} catch (e) {
|
||||
console.error('Error resetting local model:', e);
|
||||
|
|
@ -1083,11 +1069,13 @@ export default function SettingModels() {
|
|||
vllm: vllmImage,
|
||||
sglang: sglangImage,
|
||||
lmstudio: lmstudioImage,
|
||||
[LLAMA_CPP_PROVIDER_ID]: llamaCppImage,
|
||||
// Local model tab IDs
|
||||
'local-ollama': ollamaImage,
|
||||
'local-vllm': vllmImage,
|
||||
'local-sglang': sglangImage,
|
||||
'local-lmstudio': lmstudioImage,
|
||||
'local-llama.cpp': llamaCppImage,
|
||||
};
|
||||
return modelImageMap[modelId] || null;
|
||||
};
|
||||
|
|
@ -1115,13 +1103,13 @@ export default function SettingModels() {
|
|||
<button
|
||||
key={tabId}
|
||||
onClick={() => setSelectedTab(tabId)}
|
||||
className={`rounded-xl px-3 py-2 flex w-full items-center justify-between transition-all duration-200 ${isSubItem ? 'pl-3' : ''} ${
|
||||
className={`flex w-full items-center justify-between rounded-xl px-3 py-2 transition-all duration-200 ${isSubItem ? 'pl-3' : ''} ${
|
||||
isActive
|
||||
? 'bg-fill-fill-transparent-active'
|
||||
: 'bg-fill-fill-transparent hover:bg-fill-fill-transparent-hover'
|
||||
} `}
|
||||
>
|
||||
<div className="gap-3 flex items-center justify-center">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
{modelImage ? (
|
||||
<img
|
||||
src={modelImage}
|
||||
|
|
@ -1141,7 +1129,7 @@ export default function SettingModels() {
|
|||
</span>
|
||||
</div>
|
||||
{isConfigured && (
|
||||
<div className="m-1 h-2 w-2 bg-text-success rounded-full" />
|
||||
<div className="m-1 h-2 w-2 rounded-full bg-text-success" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
|
@ -1153,16 +1141,16 @@ export default function SettingModels() {
|
|||
if (selectedTab === 'cloud') {
|
||||
if (import.meta.env.VITE_USE_LOCAL_PROXY === 'true') {
|
||||
return (
|
||||
<div className="h-64 text-text-label flex items-center justify-center">
|
||||
<div className="flex h-64 items-center justify-center text-text-label">
|
||||
{t('setting.cloud-not-available-in-local-proxy')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="rounded-2xl bg-surface-tertiary flex w-full flex-col">
|
||||
<div className="mx-6 mb-4 border-border-secondary pb-4 pt-2 flex flex-col justify-start self-stretch border-x-0 border-t-0 border-b-[0.5px] border-solid">
|
||||
<div className="gap-2 inline-flex items-center justify-start self-stretch">
|
||||
<div className="text-body-base my-2 font-bold text-text-heading flex-1 justify-center">
|
||||
<div className="flex w-full flex-col rounded-2xl bg-surface-tertiary">
|
||||
<div className="mx-6 mb-4 flex flex-col justify-start self-stretch border-x-0 border-b-[0.5px] border-t-0 border-solid border-border-secondary pb-4 pt-2">
|
||||
<div className="inline-flex items-center justify-start gap-2 self-stretch">
|
||||
<div className="text-body-base my-2 flex-1 justify-center font-bold text-text-heading">
|
||||
{t('setting.eigent-cloud')}
|
||||
</div>
|
||||
{cloudPrefer ? (
|
||||
|
|
@ -1181,7 +1169,7 @@ export default function SettingModels() {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="!text-text-label rounded-full"
|
||||
className="rounded-full !text-text-label"
|
||||
onClick={() => {
|
||||
setLocalPrefer(false);
|
||||
setActiveModelIdx(null);
|
||||
|
|
@ -1205,7 +1193,7 @@ export default function SettingModels() {
|
|||
onClick={() => {
|
||||
window.location.href = `https://www.eigent.ai/pricing`;
|
||||
}}
|
||||
className="text-body-sm text-text-label cursor-pointer underline"
|
||||
className="cursor-pointer text-body-sm text-text-label underline"
|
||||
>
|
||||
{t('setting.pricing-options')}
|
||||
</span>
|
||||
|
|
@ -1215,7 +1203,7 @@ export default function SettingModels() {
|
|||
</div>
|
||||
</div>
|
||||
{/*Content Area*/}
|
||||
<div className="gap-4 px-6 pb-4 flex w-full flex-row items-center justify-between">
|
||||
<div className="flex w-full flex-row items-center justify-between gap-4 px-6 pb-4">
|
||||
<div className="text-body-sm text-text-body">
|
||||
{t('setting.credits')}:{' '}
|
||||
{loadingCredits ? (
|
||||
|
|
@ -1240,9 +1228,9 @@ export default function SettingModels() {
|
|||
<Settings />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="px-6 pb-4 flex w-full flex-1 items-center justify-between">
|
||||
<div className="min-w-0 flex flex-1 items-center">
|
||||
<span className="text-body-sm overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<div className="flex w-full flex-1 items-center justify-between px-6 pb-4">
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-body-sm">
|
||||
{t('setting.select-model-type')}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -1306,15 +1294,15 @@ export default function SettingModels() {
|
|||
const canSwitch = !!form[idx].provider_id;
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl bg-surface-tertiary flex w-full flex-col">
|
||||
<div className="mx-6 mb-4 border-border-secondary pb-4 pt-2 flex flex-col items-start justify-between border-x-0 border-t-0 border-b-[0.5px] border-solid">
|
||||
<div className="gap-2 inline-flex items-center justify-between self-stretch">
|
||||
<div className="flex w-full flex-col rounded-2xl bg-surface-tertiary">
|
||||
<div className="mx-6 mb-4 flex flex-col items-start justify-between border-x-0 border-b-[0.5px] border-t-0 border-solid border-border-secondary pb-4 pt-2">
|
||||
<div className="inline-flex items-center justify-between gap-2 self-stretch">
|
||||
<div className="text-body-base my-2 font-bold text-text-heading">
|
||||
{item.name}
|
||||
</div>
|
||||
<div className="gap-2 flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
{form[idx].prefer ? (
|
||||
<span className="px-2 py-1 text-label-xs font-bold text-text-success inline-flex items-center rounded-full">
|
||||
<span className="inline-flex items-center rounded-full px-2 py-1 text-label-xs font-bold text-text-success">
|
||||
{t('setting.default')}
|
||||
</span>
|
||||
) : (
|
||||
|
|
@ -1325,8 +1313,8 @@ export default function SettingModels() {
|
|||
onClick={() => handleSwitch(idx, true)}
|
||||
className={
|
||||
canSwitch
|
||||
? 'bg-button-transparent-fill-hover !text-text-label hover:bg-button-transparent-fill-active inline-flex items-center rounded-full shadow-none'
|
||||
: 'gap-1.5 inline-flex items-center'
|
||||
? 'inline-flex items-center rounded-full bg-button-transparent-fill-hover !text-text-label shadow-none hover:bg-button-transparent-fill-active'
|
||||
: 'inline-flex items-center gap-1.5'
|
||||
}
|
||||
>
|
||||
{!canSwitch
|
||||
|
|
@ -1335,9 +1323,9 @@ export default function SettingModels() {
|
|||
</Button>
|
||||
)}
|
||||
{form[idx].provider_id ? (
|
||||
<div className="h-2 w-2 bg-text-success shrink-0 rounded-full" />
|
||||
<div className="h-2 w-2 shrink-0 rounded-full bg-text-success" />
|
||||
) : (
|
||||
<div className="h-2 w-2 bg-text-label shrink-0 rounded-full opacity-10" />
|
||||
<div className="h-2 w-2 shrink-0 rounded-full bg-text-label opacity-10" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1345,7 +1333,7 @@ export default function SettingModels() {
|
|||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="gap-4 px-6 flex w-full flex-col items-center">
|
||||
<div className="flex w-full flex-col items-center gap-4 px-6">
|
||||
{/* API Key Setting */}
|
||||
<Input
|
||||
id={`apiKey-${item.id}`}
|
||||
|
|
@ -1426,7 +1414,7 @@ export default function SettingModels() {
|
|||
{item.externalConfig &&
|
||||
form[idx].externalConfig &&
|
||||
form[idx].externalConfig.map((ec, ecIdx) => (
|
||||
<div key={ec.key} className="gap-4 flex h-full w-full flex-col">
|
||||
<div key={ec.key} className="flex h-full w-full flex-col gap-4">
|
||||
{ec.options && ec.options.length > 0 ? (
|
||||
<Select
|
||||
value={ec.value}
|
||||
|
|
@ -1493,7 +1481,7 @@ export default function SettingModels() {
|
|||
))}
|
||||
</div>
|
||||
{/* Action Button */}
|
||||
<div className="gap-2 px-6 py-4 flex justify-end">
|
||||
<div className="flex justify-end gap-2 px-6 py-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
|
@ -1524,20 +1512,21 @@ export default function SettingModels() {
|
|||
const currentType = localTypes[platform] || '';
|
||||
const isConfigured = !!localProviderIds[platform];
|
||||
const isPreferred = localPrefer && localPlatform === platform;
|
||||
const platformState = platformModelState[platform];
|
||||
const isModelListPlatform = !!LOCAL_MODEL_OPTIONS.find(
|
||||
(m) => m.id === platform && m.fetchPath
|
||||
);
|
||||
const platformModels = platformState?.models || [];
|
||||
const platformModelsLoading = platformState?.loading || false;
|
||||
const platformModelsError = platformState?.error || null;
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl bg-surface-tertiary flex w-full flex-col">
|
||||
<div className="mx-6 mb-4 border-border-secondary pb-4 pt-2 flex flex-col items-start justify-between border-x-0 border-t-0 border-b-[0.5px] border-solid">
|
||||
<div className="gap-2 inline-flex items-center justify-between self-stretch">
|
||||
<div className="gap-2 flex items-center">
|
||||
<div className="flex w-full flex-col rounded-2xl bg-surface-tertiary">
|
||||
<div className="mx-6 mb-4 flex flex-col items-start justify-between border-x-0 border-b-[0.5px] border-t-0 border-solid border-border-secondary pb-4 pt-2">
|
||||
<div className="inline-flex items-center justify-between gap-2 self-stretch">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-body-base my-2 font-bold text-text-heading">
|
||||
{platform === 'ollama'
|
||||
? 'Ollama'
|
||||
: platform === 'vllm'
|
||||
? 'vLLM'
|
||||
: platform === 'sglang'
|
||||
? 'SGLang'
|
||||
: 'LM Studio'}
|
||||
{getLocalPlatformName(platform)}
|
||||
</div>
|
||||
{isPreferred ? (
|
||||
<Button
|
||||
|
|
@ -1557,7 +1546,7 @@ export default function SettingModels() {
|
|||
onClick={() => handleLocalSwitch(true)}
|
||||
className={
|
||||
isConfigured
|
||||
? 'bg-button-transparent-fill-hover !text-text-label rounded-full shadow-none'
|
||||
? 'rounded-full bg-button-transparent-fill-hover !text-text-label shadow-none'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
|
|
@ -1568,14 +1557,14 @@ export default function SettingModels() {
|
|||
)}
|
||||
</div>
|
||||
{isConfigured ? (
|
||||
<div className="h-2 w-2 bg-text-success rounded-full" />
|
||||
<div className="h-2 w-2 rounded-full bg-text-success" />
|
||||
) : (
|
||||
<div className="h-2 w-2 bg-text-label rounded-full opacity-10" />
|
||||
<div className="h-2 w-2 rounded-full bg-text-label opacity-10" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Model Endpoint URL Setting */}
|
||||
<div className="gap-4 px-6 flex w-full flex-col items-center">
|
||||
<div className="flex w-full flex-col items-center gap-4 px-6">
|
||||
<Input
|
||||
size="default"
|
||||
title={t('setting.model-endpoint-url')}
|
||||
|
|
@ -1588,14 +1577,12 @@ export default function SettingModels() {
|
|||
}));
|
||||
setLocalInputError(false);
|
||||
setLocalError(null);
|
||||
// Clear Ollama models error when endpoint changes
|
||||
if (platform === 'ollama') {
|
||||
setOllamaModelsError(null);
|
||||
}
|
||||
// Clear model list error when endpoint changes
|
||||
clearPlatformModelsError(platform);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
if (
|
||||
platform !== 'ollama' ||
|
||||
platform !== OLLAMA_PROVIDER_ID ||
|
||||
ollamaEndpointAutoFixedOnce ||
|
||||
!canAutoFixOllamaEndpoint(e.target.value)
|
||||
) {
|
||||
|
|
@ -1614,17 +1601,13 @@ export default function SettingModels() {
|
|||
}}
|
||||
disabled={!localEnabled}
|
||||
placeholder={
|
||||
platform === 'ollama'
|
||||
? 'http://localhost:11434/v1'
|
||||
: platform === 'lmstudio'
|
||||
? 'http://localhost:1234/v1'
|
||||
: 'http://localhost:8000/v1'
|
||||
getDefaultLocalEndpoint(platform) || 'http://localhost:8000/v1'
|
||||
}
|
||||
note={localError ?? undefined}
|
||||
/>
|
||||
{platform === 'ollama' ? (
|
||||
<div className="gap-1 flex w-full flex-col">
|
||||
<div className="gap-2 flex w-full items-end">
|
||||
{isModelListPlatform ? (
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<div className="flex w-full items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={currentType}
|
||||
|
|
@ -1634,20 +1617,20 @@ export default function SettingModels() {
|
|||
[platform]: v,
|
||||
}))
|
||||
}
|
||||
disabled={!localEnabled || ollamaModelsLoading}
|
||||
disabled={!localEnabled || platformModelsLoading}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="default"
|
||||
title={t('setting.model-type')}
|
||||
state={
|
||||
localInputError || ollamaModelsError
|
||||
localInputError || platformModelsError
|
||||
? 'error'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
ollamaModelsLoading
|
||||
platformModelsLoading
|
||||
? 'Loading models...'
|
||||
: 'Select model'
|
||||
}
|
||||
|
|
@ -1656,10 +1639,10 @@ export default function SettingModels() {
|
|||
<SelectContent>
|
||||
{(() => {
|
||||
const modelList =
|
||||
currentType && !ollamaModels.includes(currentType)
|
||||
? [currentType, ...ollamaModels]
|
||||
currentType && !platformModels.includes(currentType)
|
||||
? [currentType, ...platformModels]
|
||||
: [
|
||||
...new Set([currentType, ...ollamaModels]),
|
||||
...new Set([currentType, ...platformModels]),
|
||||
].filter(Boolean);
|
||||
return modelList.length > 0 ? (
|
||||
modelList.map((model) => (
|
||||
|
|
@ -1680,23 +1663,24 @@ export default function SettingModels() {
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
fetchOllamaModels(
|
||||
currentEndpoint || DEFAULT_OLLAMA_ENDPOINT
|
||||
void fetchModelsForPlatform(
|
||||
platform,
|
||||
currentEndpoint || getDefaultLocalEndpoint(platform)
|
||||
)
|
||||
}
|
||||
disabled={!localEnabled || ollamaModelsLoading}
|
||||
disabled={!localEnabled || platformModelsLoading}
|
||||
className="mb-1 flex-shrink-0"
|
||||
>
|
||||
{ollamaModelsLoading ? (
|
||||
{platformModelsLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{ollamaModelsError && (
|
||||
{platformModelsError && (
|
||||
<span className="text-label-sm text-text-error">
|
||||
{ollamaModelsError}
|
||||
{platformModelsError}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -1718,7 +1702,7 @@ export default function SettingModels() {
|
|||
)}
|
||||
</div>
|
||||
{/* Action Button */}
|
||||
<div className="gap-2 px-6 py-4 flex justify-end">
|
||||
<div className="flex justify-end gap-2 px-6 py-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
|
@ -1748,8 +1732,8 @@ export default function SettingModels() {
|
|||
return (
|
||||
<div className="m-auto flex h-auto w-full flex-1 flex-col">
|
||||
{/* Header Section */}
|
||||
<div className="top-0 bg-surface-primary px-6 pb-6 pt-8 sticky z-10 flex w-full items-center justify-between">
|
||||
<div className="gap-4 flex w-full flex-col items-start justify-between">
|
||||
<div className="sticky top-0 z-10 flex w-full items-center justify-between bg-surface-primary px-6 pb-6 pt-8">
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4">
|
||||
<div className="flex flex-col">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('setting.models')}
|
||||
|
|
@ -1758,10 +1742,10 @@ export default function SettingModels() {
|
|||
</div>
|
||||
</div>
|
||||
{/* Content Section */}
|
||||
<div className="mb-8 gap-6 flex flex-col">
|
||||
<div className="mb-8 flex flex-col gap-6">
|
||||
{/* Default Model Cascading Dropdown */}
|
||||
<div className="gap-4 rounded-2xl bg-surface-secondary px-6 py-4 flex w-full flex-col items-end justify-between">
|
||||
<div className="gap-1 flex w-full flex-col items-start justify-center">
|
||||
<div className="flex w-full flex-col items-end justify-between gap-4 rounded-2xl bg-surface-secondary px-6 py-4">
|
||||
<div className="flex w-full flex-col items-start justify-center gap-1">
|
||||
<div className="text-body-base font-bold text-text-heading">
|
||||
{t('setting.models-default-setting-title')}
|
||||
</div>
|
||||
|
|
@ -1771,11 +1755,11 @@ export default function SettingModels() {
|
|||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="gap-2 rounded-lg border-border-success bg-surface-success px-3 py-1 font-semibold text-text-success flex w-fit items-center justify-between border-[0.5px] border-solid transition-colors hover:opacity-70 active:opacity-90">
|
||||
<span className="text-body-sm whitespace-nowrap">
|
||||
<button className="flex w-fit items-center justify-between gap-2 rounded-lg border-[0.5px] border-solid border-border-success bg-surface-success px-3 py-1 font-semibold text-text-success transition-colors hover:opacity-70 active:opacity-90">
|
||||
<span className="whitespace-nowrap text-body-sm">
|
||||
{getDefaultModelDisplayText()}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 text-text-success flex-shrink-0" />
|
||||
<ChevronDown className="h-4 w-4 flex-shrink-0 text-text-success" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[180px]">
|
||||
|
|
@ -1829,7 +1813,7 @@ export default function SettingModels() {
|
|||
}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="gap-2 flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
{modelImage ? (
|
||||
<img
|
||||
src={modelImage}
|
||||
|
|
@ -1850,15 +1834,15 @@ export default function SettingModels() {
|
|||
{item.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="gap-1 flex items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
{!isConfigured && (
|
||||
<div className="h-2 w-2 bg-text-label rounded-full opacity-10" />
|
||||
<div className="h-2 w-2 rounded-full bg-text-label opacity-10" />
|
||||
)}
|
||||
{isPreferred && (
|
||||
<Check className="h-4 w-4 text-text-success" />
|
||||
)}
|
||||
{isConfigured && !isPreferred && (
|
||||
<div className="h-2 w-2 bg-text-success rounded-full" />
|
||||
<div className="h-2 w-2 rounded-full bg-text-success" />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -1876,7 +1860,7 @@ export default function SettingModels() {
|
|||
</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-[200px]">
|
||||
{localModelOptions.map((model) => {
|
||||
{LOCAL_MODEL_OPTIONS.map((model) => {
|
||||
const isConfigured = !!localProviderIds[model.id];
|
||||
const isPreferred =
|
||||
localPrefer && localPlatform === model.id;
|
||||
|
|
@ -1890,7 +1874,7 @@ export default function SettingModels() {
|
|||
}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="gap-2 flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
{modelImage ? (
|
||||
<img
|
||||
src={modelImage}
|
||||
|
|
@ -1911,15 +1895,15 @@ export default function SettingModels() {
|
|||
{model.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="gap-1 flex items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
{!isConfigured && (
|
||||
<div className="h-2 w-2 bg-text-label rounded-full opacity-10" />
|
||||
<div className="h-2 w-2 rounded-full bg-text-label opacity-10" />
|
||||
)}
|
||||
{isPreferred && (
|
||||
<Check className="h-4 w-4 text-text-success" />
|
||||
)}
|
||||
{isConfigured && !isPreferred && (
|
||||
<div className="h-2 w-2 bg-text-success rounded-full" />
|
||||
<div className="h-2 w-2 rounded-full bg-text-success" />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -1932,17 +1916,17 @@ export default function SettingModels() {
|
|||
</div>
|
||||
|
||||
{/* Content Section with Sidebar */}
|
||||
<div className="gap-2 rounded-2xl bg-surface-secondary px-6 py-4 flex w-full flex-col items-start justify-between">
|
||||
<div className="text-body-base mb-2 border-border-secondary bg-surface-secondary pb-4 font-bold text-text-heading sticky top-[86px] z-10 w-full border-x-0 border-t-0 border-b-[0.5px] border-solid">
|
||||
<div className="flex w-full flex-col items-start justify-between gap-2 rounded-2xl bg-surface-secondary px-6 py-4">
|
||||
<div className="text-body-base sticky top-[86px] z-10 mb-2 w-full border-x-0 border-b-[0.5px] border-t-0 border-solid border-border-secondary bg-surface-secondary pb-4 font-bold text-text-heading">
|
||||
{t('setting.models-configuration')}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-row items-start justify-between">
|
||||
{/* Sidebar */}
|
||||
<div className="-ml-2 mr-4 rounded-2xl bg-surface-secondary h-full w-[240px]">
|
||||
<div className="gap-4 flex flex-col">
|
||||
<div className="-ml-2 mr-4 h-full w-[240px] rounded-2xl bg-surface-secondary">
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Eigent Cloud Section */}
|
||||
<div className="gap-1 flex flex-col">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="px-3 py-2 text-body-sm font-bold text-text-heading">
|
||||
{t('setting.eigent-cloud')}
|
||||
</div>
|
||||
|
|
@ -1957,10 +1941,10 @@ export default function SettingModels() {
|
|||
)}
|
||||
</div>
|
||||
{/* Bring Your Own Key Section */}
|
||||
<div className="gap-1 flex flex-col">
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={() => setByokCollapsed(!byokCollapsed)}
|
||||
className="rounded-lg px-3 py-2 hover:bg-surface-secondary flex items-center justify-between bg-transparent transition-colors"
|
||||
className="flex items-center justify-between rounded-lg bg-transparent px-3 py-2 transition-colors hover:bg-surface-secondary"
|
||||
>
|
||||
<div className="text-body-sm font-bold text-text-heading">
|
||||
{t('setting.custom-model')}
|
||||
|
|
@ -1972,7 +1956,7 @@ export default function SettingModels() {
|
|||
)}
|
||||
</button>
|
||||
<div
|
||||
className={`ease-in-out overflow-hidden transition-all duration-300 ${
|
||||
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
||||
byokCollapsed
|
||||
? 'max-h-0 opacity-0'
|
||||
: 'max-h-[2000px] opacity-100'
|
||||
|
|
@ -1992,10 +1976,10 @@ export default function SettingModels() {
|
|||
</div>
|
||||
|
||||
{/* Local Model Section */}
|
||||
<div className="gap-1 flex flex-col">
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={() => setLocalCollapsed(!localCollapsed)}
|
||||
className="rounded-lg px-3 py-2 hover:bg-surface-secondary flex items-center justify-between bg-transparent transition-colors"
|
||||
className="flex items-center justify-between rounded-lg bg-transparent px-3 py-2 transition-colors hover:bg-surface-secondary"
|
||||
>
|
||||
<div className="text-body-sm font-bold text-text-heading">
|
||||
{t('setting.local-model')}
|
||||
|
|
@ -2007,7 +1991,7 @@ export default function SettingModels() {
|
|||
)}
|
||||
</button>
|
||||
<div
|
||||
className={`ease-in-out overflow-hidden transition-all duration-300 ${
|
||||
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
||||
localCollapsed
|
||||
? 'max-h-0 opacity-0'
|
||||
: 'max-h-[2000px] opacity-100'
|
||||
|
|
@ -2019,7 +2003,7 @@ export default function SettingModels() {
|
|||
'local-ollama',
|
||||
selectedTab === 'local-ollama',
|
||||
true,
|
||||
!!localProviderIds['ollama']
|
||||
!!localProviderIds[OLLAMA_PROVIDER_ID]
|
||||
)}
|
||||
{renderSidebarItem(
|
||||
'local-vllm',
|
||||
|
|
@ -2027,7 +2011,7 @@ export default function SettingModels() {
|
|||
'local-vllm',
|
||||
selectedTab === 'local-vllm',
|
||||
true,
|
||||
!!localProviderIds['vllm']
|
||||
!!localProviderIds[VLLM_PROVIDER_ID]
|
||||
)}
|
||||
{renderSidebarItem(
|
||||
'local-sglang',
|
||||
|
|
@ -2035,7 +2019,7 @@ export default function SettingModels() {
|
|||
'local-sglang',
|
||||
selectedTab === 'local-sglang',
|
||||
true,
|
||||
!!localProviderIds['sglang']
|
||||
!!localProviderIds[SGLANG_PROVIDER_ID]
|
||||
)}
|
||||
{renderSidebarItem(
|
||||
'local-lmstudio',
|
||||
|
|
@ -2043,14 +2027,22 @@ export default function SettingModels() {
|
|||
'local-lmstudio',
|
||||
selectedTab === 'local-lmstudio',
|
||||
true,
|
||||
!!localProviderIds['lmstudio']
|
||||
!!localProviderIds[LMSTUDIO_PROVIDER_ID]
|
||||
)}
|
||||
{renderSidebarItem(
|
||||
'local-llama.cpp',
|
||||
'LLaMA.cpp',
|
||||
'local-llama.cpp',
|
||||
selectedTab === 'local-llama.cpp',
|
||||
true,
|
||||
!!localProviderIds[LLAMA_CPP_PROVIDER_ID]
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Main Content */}
|
||||
<div className="min-w-0 sticky top-[136px] z-10 flex-1">
|
||||
<div className="sticky top-[136px] z-10 min-w-0 flex-1">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
163
src/pages/Agents/localModels.ts
Normal file
163
src/pages/Agents/localModels.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
// --- Provider IDs ---
|
||||
|
||||
export const OLLAMA_PROVIDER_ID = 'ollama' as const;
|
||||
export const VLLM_PROVIDER_ID = 'vllm' as const;
|
||||
export const SGLANG_PROVIDER_ID = 'sglang' as const;
|
||||
export const LMSTUDIO_PROVIDER_ID = 'lmstudio' as const;
|
||||
export const LLAMA_CPP_PROVIDER_ID = 'llama.cpp' as const;
|
||||
|
||||
// --- Ollama endpoint auto-fix ---
|
||||
|
||||
// Toast strings shown when the Ollama endpoint input auto-appends "/v1".
|
||||
// Triggered on blur when a user enters a bare Ollama URL (e.g. http://localhost:11434)
|
||||
// that is missing the required /v1 suffix for the OpenAI-compatible API.
|
||||
export const OLLAMA_ENDPOINT_AUTO_FIX_TITLE = 'Ollama endpoint updated';
|
||||
export const OLLAMA_ENDPOINT_AUTO_FIX_DESC =
|
||||
'Added /v1 once. You can remove it if not needed.';
|
||||
|
||||
// --- Local model config ---
|
||||
|
||||
// Model fetch config per local provider.
|
||||
// - fetchPath: the API path (relative to the base URL) to list available models.
|
||||
// - parseModels: extracts model name strings from the JSON response.
|
||||
// Ollama uses a proprietary /api/tags endpoint; LLaMA.cpp uses the OpenAI-compatible
|
||||
// /v1/models endpoint. vLLM, SGLang, and LM Studio also expose /v1/models and can
|
||||
// be added here to enable model listing for those providers.
|
||||
export type LocalModelOption = {
|
||||
id: string;
|
||||
name: string;
|
||||
defaultEndpoint: string;
|
||||
fetchPath?: string;
|
||||
parseModels?: (data: any) => string[];
|
||||
};
|
||||
|
||||
const parseOllamaModels = (data: any): string[] =>
|
||||
data.models?.map((m: any) => m.name) || [];
|
||||
|
||||
const parseOpenAICompatibleModels = (data: any): string[] =>
|
||||
data?.data
|
||||
?.map((m: any) => m?.id)
|
||||
.filter((name: string | undefined) => !!name) || [];
|
||||
|
||||
export const LOCAL_MODEL_OPTIONS: LocalModelOption[] = [
|
||||
{
|
||||
id: OLLAMA_PROVIDER_ID,
|
||||
name: 'Ollama',
|
||||
defaultEndpoint: 'http://localhost:11434/v1',
|
||||
fetchPath: '/api/tags',
|
||||
parseModels: parseOllamaModels,
|
||||
},
|
||||
{
|
||||
id: VLLM_PROVIDER_ID,
|
||||
name: 'vLLM',
|
||||
defaultEndpoint: 'http://localhost:8000/v1',
|
||||
},
|
||||
{
|
||||
id: SGLANG_PROVIDER_ID,
|
||||
name: 'SGLang',
|
||||
defaultEndpoint: 'http://localhost:30000/v1',
|
||||
},
|
||||
{
|
||||
id: LMSTUDIO_PROVIDER_ID,
|
||||
name: 'LM Studio',
|
||||
defaultEndpoint: 'http://localhost:1234/v1',
|
||||
},
|
||||
{
|
||||
id: LLAMA_CPP_PROVIDER_ID,
|
||||
name: 'LLaMA.cpp',
|
||||
defaultEndpoint: 'http://localhost:8080/v1',
|
||||
// Uses the OpenAI-compatible /v1/models endpoint.
|
||||
// vLLM, SGLang, and LM Studio also support this same endpoint — to enable
|
||||
// model listing for them, add fetchPath and parseModels to their entries above.
|
||||
fetchPath: '/v1/models',
|
||||
parseModels: parseOpenAICompatibleModels,
|
||||
},
|
||||
];
|
||||
|
||||
// Provider logos that use dark fills (black or currentColor) and need inversion in dark mode
|
||||
export const DARK_FILL_MODELS = new Set([
|
||||
'openai',
|
||||
'anthropic',
|
||||
'moonshot',
|
||||
OLLAMA_PROVIDER_ID,
|
||||
'openrouter',
|
||||
LMSTUDIO_PROVIDER_ID,
|
||||
'z.ai',
|
||||
'openai-compatible-model',
|
||||
]);
|
||||
|
||||
export const PROVIDER_AVATAR_URLS: Record<string, string> = {
|
||||
'samba-nova': 'https://github.com/sambanova.png',
|
||||
mistral: 'https://github.com/mistralai.png',
|
||||
grok: 'https://github.com/xai-org.png',
|
||||
};
|
||||
|
||||
// --- Helper functions ---
|
||||
|
||||
// Strip trailing /v1 (and optional slash) to get the base server URL.
|
||||
// e.g. "http://localhost:8080/v1" -> "http://localhost:8080"
|
||||
export const toEndpointBaseUrl = (endpoint: string): string =>
|
||||
endpoint.replace(/\/v1\/?$/, '').replace(/\/$/, '');
|
||||
|
||||
// Look up the default endpoint URL for a local provider by its platform ID.
|
||||
export const getDefaultLocalEndpoint = (platform: string): string =>
|
||||
LOCAL_MODEL_OPTIONS.find((model) => model.id === platform)?.defaultEndpoint ||
|
||||
'';
|
||||
|
||||
// Look up the display name for a local provider by its platform ID.
|
||||
export const getLocalPlatformName = (platform: string): string =>
|
||||
LOCAL_MODEL_OPTIONS.find((m) => m.id === platform)?.name || platform;
|
||||
|
||||
// --- Ollama endpoint helpers ---
|
||||
|
||||
export const isOllamaEndpointMissingV1 = (endpoint: string): boolean => {
|
||||
const trimmed = endpoint.trim();
|
||||
if (!trimmed) return false;
|
||||
try {
|
||||
const normalizedPath = new URL(trimmed).pathname.replace(/\/+$/, '');
|
||||
return !normalizedPath.endsWith('/v1');
|
||||
} catch {
|
||||
return !trimmed.replace(/\/+$/, '').endsWith('/v1');
|
||||
}
|
||||
};
|
||||
|
||||
export const canAutoFixOllamaEndpoint = (endpoint: string): boolean => {
|
||||
const trimmed = endpoint.trim();
|
||||
if (!trimmed || !isOllamaEndpointMissingV1(trimmed)) return false;
|
||||
try {
|
||||
// Auto-fix only when endpoint has no extra path, e.g. http://localhost:11434
|
||||
const normalizedPath = new URL(trimmed).pathname.replace(/\/+$/, '');
|
||||
return normalizedPath === '';
|
||||
} catch {
|
||||
const withoutQueryOrHash = trimmed.split(/[?#]/)[0] || '';
|
||||
const normalized = withoutQueryOrHash.replace(/\/+$/, '');
|
||||
return !normalized.includes('/');
|
||||
}
|
||||
};
|
||||
|
||||
export const appendV1ToEndpoint = (endpoint: string): string => {
|
||||
const trimmed = endpoint.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
const normalizedPath = parsed.pathname.replace(/\/+$/, '');
|
||||
parsed.pathname = `${normalizedPath}/v1`.replace(/\/{2,}/g, '/');
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return `${trimmed.replace(/\/+$/, '')}/v1`;
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue