diff --git a/.env.example b/.env.example index 970216e11..96fc397f3 100644 --- a/.env.example +++ b/.env.example @@ -83,6 +83,9 @@ OPENROUTERAPIKEY='' # If you want to use IO.net, add your key here IONETAPIKEY='' +# If you want to use Morpheus decentralized AI, add your key here +MORPHEUS_API_KEY='' + ######################################## # API Rate Limiting Configuration RATE_LIMIT_ENABLED='false' # Enable/disable API key rate limiting diff --git a/backend/node/create_nodes/providers_schema.json b/backend/node/create_nodes/providers_schema.json index 29e348651..165bccde9 100644 --- a/backend/node/create_nodes/providers_schema.json +++ b/backend/node/create_nodes/providers_schema.json @@ -12,7 +12,9 @@ "anthropic", "xai", "google", - "openrouter" + "openrouter", + "ionet", + "morpheus" ] }, "model": { @@ -216,6 +218,25 @@ } } }, + { + "if": { + "properties": { + "provider": { "const": "morpheus" } + } + }, + "then": { + "x-models-url": "https://api.mor.org/api/v1/models", + "x-plugin-config-defaults": { + "api_key_env_var": "MORPHEUS_API_KEY", + "api_url": "https://api.mor.org/api/v1" + }, + "properties": { + "plugin": { + "const": "openai-compatible" + } + } + } + }, { "if": { "properties": { diff --git a/frontend/src/assets/schemas/providers_schema.json b/frontend/src/assets/schemas/providers_schema.json index 57de3ce2f..d82068c85 100644 --- a/frontend/src/assets/schemas/providers_schema.json +++ b/frontend/src/assets/schemas/providers_schema.json @@ -11,7 +11,10 @@ "openai", "anthropic", "xai", - "google" + "google", + "openrouter", + "ionet", + "morpheus" ] }, "model": { @@ -102,6 +105,61 @@ } } }, + { + "if": { + "properties": { + "provider": { "const": "openrouter" } + } + }, + "then": { + "properties": { + "plugin": { + "const": "openai-compatible" + }, + "model": { + "enum": [ + "@preset/rally-testnet-gpt-5-1", + "@preset/rally-testnet-sonnet-4-5", + "@preset/rally-testnet-gemini-3-flash", + "openai/gpt-5.2", + "openai/gpt-5.1", + "openai/gpt-5-mini", + "z-ai/glm-4.7", + "moonshotai/kimi-k2-0905", + "moonshotai/kimi-k2.5", + "deepseek/deepseek-v3.2", + "qwen/qwen3-235b-a22b-2507", + "anthropic/claude-sonnet-4.5", + "google/gemini-3-flash-preview", + "x-ai/grok-4", + "mistralai/mistral-large-2512", + "meta-llama/llama-4-maverick", + "nvidia/llama-3.1-nemotron-ultra-253b-v1" + ] + } + } + } + }, + { + "if": { + "properties": { + "provider": { "const": "ionet" } + } + }, + "then": { + "properties": { + "plugin": { + "const": "openai-compatible" + }, + "model": { + "enum": [ + "deepseek-ai/DeepSeek-V3.2", + "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8" + ] + } + } + } + }, { "if": { "properties": { @@ -150,6 +208,25 @@ } } }, + { + "if": { + "properties": { + "provider": { "const": "morpheus" } + } + }, + "then": { + "x-models-url": "https://api.mor.org/api/v1/models", + "x-plugin-config-defaults": { + "api_key_env_var": "MORPHEUS_API_KEY", + "api_url": "https://api.mor.org/api/v1" + }, + "properties": { + "plugin": { + "const": "openai-compatible" + } + } + } + }, { "if": { "properties": { diff --git a/frontend/src/components/Simulator/ProviderModal.vue b/frontend/src/components/Simulator/ProviderModal.vue index 0266b6557..51849cd43 100644 --- a/frontend/src/components/Simulator/ProviderModal.vue +++ b/frontend/src/components/Simulator/ProviderModal.vue @@ -98,6 +98,8 @@ const pluginOptions = ref([]); const modelOptions = ref([]); const isPluginLocked = ref(false); const customProvider = ref(false); +const isLoadingModels = ref(false); +const modelFetchError = ref(''); const providerOptions = ref([]); const availablePluginOptions = computed(() => { @@ -201,6 +203,30 @@ function extractDefaults( return defaults; } +const fetchDynamicModels = async (modelsUrl: string) => { + isLoadingModels.value = true; + modelFetchError.value = ''; + try { + const response = await fetch(modelsUrl); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + if (data?.data && Array.isArray(data.data)) { + modelOptions.value = data.data + .filter((m: any) => !m.modelType || m.modelType === 'LLM') + .map((m: any) => m.id); + if (modelOptions.value.length > 0 && isCreateMode.value) { + newProviderData.model = modelOptions.value[0]; + } + } + } catch (err) { + console.error('Failed to fetch models:', err); + modelFetchError.value = + 'Could not fetch available models. You can type a model name manually.'; + } finally { + isLoadingModels.value = false; + } +}; + const pluginConfigProperties = ref>({}); const configProperties = ref>({}); @@ -259,6 +285,27 @@ const checkRules = () => { } } }); + + // Provider-specific post-processing (x-models-url, x-plugin-config-defaults) + schema.allOf.forEach((rule: any) => { + if (rule.if?.properties?.provider?.const === newProviderData.provider) { + // Override plugin_config with provider-specific defaults + if (rule.then?.['x-plugin-config-defaults'] && isCreateMode.value) { + Object.assign( + newProviderData.plugin_config, + rule.then['x-plugin-config-defaults'], + ); + } + // Fetch models dynamically for providers with x-models-url + if ( + rule.then?.['x-models-url'] && + modelOptions.value.length === 0 && + isCreateMode.value + ) { + fetchDynamicModels(rule.then['x-models-url']); + } + } + }); }; const toggleCustomProvider = () => { @@ -390,8 +437,15 @@ const configurationError = computed(() => { + +
+ Loading available models... +
+ + {{ modelFetchError }} + { placeholder="i.e. my-model" />