From fa4c1245a40c0b9748625e17fdb8f246dcdc8150 Mon Sep 17 00:00:00 2001 From: bowtiedbluefin <95500901+bowtiedbluefin@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:34:45 -0500 Subject: [PATCH] feat: add Morpheus Inference API provider integration Morpheus is a decentralized inference marketplace that provides FREE access to AI models during Open Beta. The API is fully OpenAI-compatible. This integration adds: - Complete model catalog with 14 models: - Flagship: Qwen3 Coder 480B, Hermes 3 Llama 405B, GPT OSS 120B - Reasoning: Kimi K2 Thinking, GLM 4.7 Thinking, Qwen3 235B - Mid-size: Llama 3.3 70B (default), Qwen3 Next 80B, Mistral 31 24B - Fast: Llama 3.2 3B, Qwen3 4B - Auto-discovery from Morpheus API with fallback to static catalog - MORPHEUS_API_KEY environment variable support - Interactive onboarding via 'morpheus-api-key' auth choice - Provider auto-registration when API key is detected - Comprehensive documentation covering: - All models with context windows and features - Streaming and function calling support - Security guidelines - Troubleshooting Default model: morpheus/llama-3.3-70b (reliable, balanced performance) Morpheus API: https://api.mor.org/api/v1 (OpenAI-compatible) --- docs/providers/index.md | 1 + docs/providers/morpheus.md | 222 +++++++++++++ src/agents/model-auth.ts | 1 + src/agents/models-config.providers.ts | 17 + src/agents/morpheus-models.ts | 310 ++++++++++++++++++ src/commands/auth-choice-options.ts | 12 + .../auth-choice.apply.api-providers.ts | 65 ++++ src/commands/onboard-auth.config-core.ts | 75 +++++ src/commands/onboard-auth.credentials.ts | 15 +- src/commands/onboard-auth.ts | 7 + src/commands/onboard-types.ts | 1 + 11 files changed, 725 insertions(+), 1 deletion(-) create mode 100644 docs/providers/morpheus.md create mode 100644 src/agents/morpheus-models.ts diff --git a/docs/providers/index.md b/docs/providers/index.md index c18ad70fb..e1358e5a2 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -45,6 +45,7 @@ See [Venice AI](/providers/venice). - [GLM models](/providers/glm) - [MiniMax](/providers/minimax) - [Venius (Venice AI, privacy-focused)](/providers/venice) +- [Morpheus (decentralized inference, FREE beta)](/providers/morpheus) - [Ollama (local models)](/providers/ollama) ## Transcription providers diff --git a/docs/providers/morpheus.md b/docs/providers/morpheus.md new file mode 100644 index 000000000..ceaecb27b --- /dev/null +++ b/docs/providers/morpheus.md @@ -0,0 +1,222 @@ +--- +summary: "Use Morpheus decentralized inference in Clawdbot" +read_when: + - You want decentralized AI inference in Clawdbot + - You want Morpheus API setup guidance +--- +# Morpheus Inference API + +**Morpheus** provides decentralized AI inference via the Morpheus Network, offering FREE access to open-source models during Open Beta. + +The Morpheus Inference API is a simple, OpenAI-compatible gateway providing users access to the Morpheus Inference Marketplace. Providers host hardware and offer inference, while the API abstracts these efforts for a seamless experience. + +## Why Morpheus in Clawdbot + +- **Decentralized inference** from the Morpheus Inference Marketplace +- **FREE during Open Beta** (until 1/31/26) +- **20+ models** including Llama, Qwen, DeepSeek, GLM, Kimi, and more +- OpenAI-compatible `/v1` endpoints + +## Features + +- **OpenAI-compatible API**: Standard `/v1` endpoints for easy integration +- **Streaming**: Supported on all models +- **Function calling**: Supported on most models +- **Vision**: Supported on select models (e.g., `mistral-31-24b`) +- **Web search**: Add `:web` suffix to any model for web search capabilities + +## Setup + +### 1. Get API Key + +1. Create an account at [app.mor.org](https://app.mor.org) +2. Click **Create API Key** and copy it immediately +3. Your API key format: `sk-xxxxxxxxxxxxx` + +### 2. Configure Clawdbot + +**Option A: Environment Variable** + +```bash +export MORPHEUS_API_KEY="sk-xxxxxxxxxxxxx" +``` + +**Option B: Interactive Setup (Recommended)** + +```bash +clawdbot onboard --auth-choice morpheus-api-key +``` + +This will: +1. Prompt for your API key (or use existing `MORPHEUS_API_KEY`) +2. Show all available Morpheus models +3. Let you pick your default model +4. Configure the provider automatically + +**Option C: Non-interactive** + +```bash +clawdbot onboard --non-interactive \ + --auth-choice morpheus-api-key \ + --morpheus-api-key "sk-xxxxxxxxxxxxx" +``` + +### 3. Verify Setup + +```bash +clawdbot chat --model morpheus/llama-3.3-70b "Hello, are you working?" +``` + +## Model Selection + +After setup, Clawdbot shows all available Morpheus models. Pick based on your needs: + +- **Default (our pick)**: `morpheus/llama-3.3-70b` for reliable, balanced performance +- **Best for coding**: `morpheus/qwen3-coder-480b-a35b-instruct` with 256K context +- **Best for reasoning**: `morpheus/kimi-k2-thinking` for deep analysis +- **Fastest**: `morpheus/llama-3.2-3b` for low-latency responses + +Change your default model anytime: + +```bash +clawdbot models set morpheus/llama-3.3-70b +clawdbot models set morpheus/kimi-k2-thinking +``` + +List all available models: + +```bash +clawdbot models list | grep morpheus +``` + +## Available Models + +### Flagship Models + +| Model ID | Name | Context | Best For | +|----------|------|---------|----------| +| `qwen3-coder-480b-a35b-instruct` | Qwen3 Coder 480B | 256K | Code generation | +| `hermes-3-llama-3.1-405b` | Hermes 3 Llama 405B | 128K | General purpose | +| `gpt-oss-120b` | GPT OSS 120B | 128K | GPT-style responses | + +### Reasoning Models + +| Model ID | Name | Context | Best For | +|----------|------|---------|----------| +| `kimi-k2-thinking` | Kimi K2 Thinking | 256K | Deep reasoning, math, coding | +| `glm-4.7-thinking` | GLM 4.7 Thinking | 198K | Extended thinking | +| `glm-4.7` | GLM 4.7 | 198K | Reasoning, multilingual | +| `qwen3-235b` | Qwen3 235B | 128K | Complex reasoning | + +### Mid-Size Models + +| Model ID | Name | Context | Best For | +|----------|------|---------|----------| +| `llama-3.3-70b` | Llama 3.3 70B | 128K | General purpose | +| `qwen3-next-80b` | Qwen3 Next 80B | 256K | Long context | +| `mistral-31-24b` | Mistral 31 24B | 128K | Fast, vision | +| `venice-uncensored` | Venice Uncensored | 32K | Uncensored, creative | +| `hermes-4-14b` | Hermes 4 14B | 128K | Efficient | + +### Fast Models + +| Model ID | Name | Context | Best For | +|----------|------|---------|----------| +| `llama-3.2-3b` | Llama 3.2 3B | 128K | Fastest responses | +| `qwen3-4b` | Qwen3 4B | 32K | Lightweight, reasoning | + +### Web-Enabled Models + +Add `:web` suffix to any model for web search: +- `llama-3.3-70b:web` +- `kimi-k2-thinking:web` +- `qwen3-coder-480b-a35b-instruct:web` + +## Model Discovery + +Clawdbot automatically discovers models from the Morpheus API when `MORPHEUS_API_KEY` is set. If the API is unreachable, it falls back to a static catalog. + +## Streaming & Tool Support + +| Feature | Support | +|---------|---------| +| **Streaming** | All models | +| **Function calling** | Most models | +| **Vision/Images** | `mistral-31-24b` | +| **JSON mode** | Supported via `response_format` | + +## Pricing + +Morpheus is **FREE during Open Beta** (until 1/31/26). Billing infrastructure will be implemented soon. + +## Config File Example + +```json5 +{ + env: { MORPHEUS_API_KEY: "sk-..." }, + agents: { defaults: { model: { primary: "morpheus/llama-3.3-70b" } } }, + models: { + mode: "merge", + providers: { + morpheus: { + baseUrl: "https://api.mor.org/api/v1", + apiKey: "${MORPHEUS_API_KEY}", + api: "openai-completions", + models: [ + { + id: "llama-3.3-70b", + name: "Llama 3.3 70B", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 8192 + } + ] + } + } + } +} +``` + +## Usage Examples + +```bash +# Use default model +clawdbot chat --model morpheus/llama-3.3-70b + +# Use coding model +clawdbot chat --model morpheus/qwen3-coder-480b-a35b-instruct + +# Use reasoning model +clawdbot chat --model morpheus/kimi-k2-thinking + +# Use with web search +clawdbot chat --model morpheus/llama-3.3-70b:web +``` + +## Troubleshooting + +### API key not recognized + +```bash +echo $MORPHEUS_API_KEY +clawdbot models list | grep morpheus +``` + +Ensure the key starts with `sk-`. + +### Model not available + +Model availability depends on active providers in the Morpheus marketplace. Run `clawdbot models list` to see currently available models. + +### Connection issues + +Morpheus API is at `https://api.mor.org/api/v1`. Ensure your network allows HTTPS connections. + +## Links + +- [Morpheus API Docs](https://apidocs.mor.org) +- [Morpheus App](https://app.mor.org) +- [Morpheus Discord](https://discord.gg/kyVaxTHnvB) +- [Morpheus Twitter](https://x.com/morpheusais) diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 96e4e4ae6..b4f2fc927 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -283,6 +283,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { minimax: "MINIMAX_API_KEY", synthetic: "SYNTHETIC_API_KEY", venice: "VENICE_API_KEY", + morpheus: "MORPHEUS_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", }; diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 76f1c3acd..1bb80040a 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -13,6 +13,7 @@ import { SYNTHETIC_MODEL_CATALOG, } from "./synthetic-models.js"; import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js"; +import { discoverMorpheusModels, MORPHEUS_BASE_URL } from "./morpheus-models.js"; type ModelsConfig = NonNullable; export type ProviderConfig = NonNullable[string]; @@ -350,6 +351,15 @@ async function buildVeniceProvider(): Promise { }; } +async function buildMorpheusProvider(apiKey?: string): Promise { + const models = await discoverMorpheusModels(apiKey); + return { + baseUrl: MORPHEUS_BASE_URL, + api: "openai-completions", + models, + }; +} + async function buildOllamaProvider(): Promise { const models = await discoverOllamaModels(); return { @@ -402,6 +412,13 @@ export async function resolveImplicitProviders(params: { providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey }; } + const morpheusKey = + resolveEnvApiKeyVarName("morpheus") ?? + resolveApiKeyFromProfiles({ provider: "morpheus", store: authStore }); + if (morpheusKey) { + providers.morpheus = { ...(await buildMorpheusProvider(morpheusKey)), apiKey: morpheusKey }; + } + const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal"); if (qwenProfiles.length > 0) { providers["qwen-portal"] = { diff --git a/src/agents/morpheus-models.ts b/src/agents/morpheus-models.ts new file mode 100644 index 000000000..37dbfe1ac --- /dev/null +++ b/src/agents/morpheus-models.ts @@ -0,0 +1,310 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; + +export const MORPHEUS_BASE_URL = "https://api.mor.org/api/v1"; +export const MORPHEUS_DEFAULT_MODEL_ID = "llama-3.3-70b"; +export const MORPHEUS_DEFAULT_MODEL_REF = `morpheus/${MORPHEUS_DEFAULT_MODEL_ID}`; + +// Morpheus is currently FREE during Open Beta (until 1/31/26). +// Set costs to 0 as pricing will be implemented later. +export const MORPHEUS_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +/** + * Complete catalog of Morpheus Inference API models. + * + * Morpheus is a decentralized inference marketplace that provides access to + * open-source AI models. The API is fully OpenAI-compatible. + * + * Model availability depends on active providers in the marketplace. + * This catalog serves as a fallback when the Morpheus API is unreachable. + * + * Models with the `:web` suffix have web search capabilities enabled. + */ +export const MORPHEUS_MODEL_CATALOG = [ + // ============================================ + // FLAGSHIP MODELS + // ============================================ + { + id: "qwen3-coder-480b-a35b-instruct", + name: "Qwen3 Coder 480B", + reasoning: false, + input: ["text"], + contextWindow: 256000, + maxTokens: 8192, + tags: ["Code"], + }, + { + id: "hermes-3-llama-3.1-405b", + name: "Hermes 3 Llama 3.1 405B", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + tags: ["General"], + }, + { + id: "gpt-oss-120b", + name: "GPT OSS 120B", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + tags: ["General"], + }, + + // ============================================ + // REASONING MODELS + // ============================================ + { + id: "kimi-k2-thinking", + name: "Kimi K2 Thinking", + reasoning: true, + input: ["text"], + contextWindow: 256000, + maxTokens: 8192, + tags: ["Reasoning", "Code"], + }, + { + id: "glm-4.7-thinking", + name: "GLM 4.7 Thinking", + reasoning: true, + input: ["text"], + contextWindow: 198000, + maxTokens: 8192, + tags: ["Reasoning"], + }, + { + id: "glm-4.7", + name: "GLM 4.7", + reasoning: true, + input: ["text"], + contextWindow: 198000, + maxTokens: 8192, + tags: ["Reasoning"], + }, + { + id: "qwen3-235b", + name: "Qwen3 235B", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + tags: ["Reasoning"], + }, + + // ============================================ + // MID-SIZE MODELS + // ============================================ + { + id: "llama-3.3-70b", + name: "Llama 3.3 70B", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + tags: ["General"], + }, + { + id: "qwen3-next-80b", + name: "Qwen3 Next 80B", + reasoning: false, + input: ["text"], + contextWindow: 256000, + maxTokens: 8192, + tags: ["General"], + }, + { + id: "mistral-31-24b", + name: "Mistral 31 24B", + reasoning: false, + input: ["text", "image"], + contextWindow: 128000, + maxTokens: 8192, + tags: ["Vision"], + }, + { + id: "venice-uncensored", + name: "Venice Uncensored", + reasoning: false, + input: ["text"], + contextWindow: 32000, + maxTokens: 8192, + tags: ["Uncensored"], + }, + { + id: "hermes-4-14b", + name: "Hermes 4 14B", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + tags: ["General"], + }, + + // ============================================ + // FAST MODELS + // ============================================ + { + id: "llama-3.2-3b", + name: "Llama 3.2 3B", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + tags: ["Fast"], + }, + { + id: "qwen3-4b", + name: "Qwen3 4B", + reasoning: true, + input: ["text"], + contextWindow: 32000, + maxTokens: 8192, + tags: ["Fast", "Reasoning"], + }, +] as const; + +export type MorpheusCatalogEntry = (typeof MORPHEUS_MODEL_CATALOG)[number]; + +/** + * Build a ModelDefinitionConfig from a Morpheus catalog entry. + */ +export function buildMorpheusModelDefinition(entry: MorpheusCatalogEntry): ModelDefinitionConfig { + return { + id: entry.id, + name: entry.name, + reasoning: entry.reasoning, + input: [...entry.input], + cost: MORPHEUS_DEFAULT_COST, + contextWindow: entry.contextWindow, + maxTokens: entry.maxTokens, + }; +} + +// Morpheus API response types +interface MorpheusModel { + id: string; + blockchainID: string; + created: number; + tags: string[]; + modelType: "LLM" | "TTS" | "STT" | "EMBEDDING"; +} + +interface MorpheusModelsResponse { + object: string; + data: MorpheusModel[]; +} + +/** + * Infer model properties from Morpheus API model data. + */ +function inferModelProperties(model: MorpheusModel): { + reasoning: boolean; + input: string[]; + contextWindow: number; +} { + const id = model.id.toLowerCase(); + const tags = model.tags.map((t) => t.toLowerCase()); + + // Infer reasoning from model name or tags + const reasoning = + id.includes("thinking") || + id.includes("reason") || + id.includes("r1") || + tags.includes("reasoning"); + + // Infer vision support from tags + const hasVision = id.includes("vision") || id.includes("vl-") || id.includes("-vl"); + const input = hasVision ? ["text", "image"] : ["text"]; + + // Infer context window from size tag or model name + let contextWindow = 128000; + if (id.includes("qwen3-coder") || id.includes("kimi-k2") || id.includes("qwen3-next")) { + contextWindow = 256000; + } else if (id.includes("glm-4.7")) { + contextWindow = 198000; + } else if (id.includes("venice-uncensored") || id.includes("qwen3-4b")) { + contextWindow = 32000; + } + + return { reasoning, input, contextWindow }; +} + +/** + * Discover models from Morpheus API with fallback to static catalog. + * The /models endpoint requires authentication. + */ +export async function discoverMorpheusModels(apiKey?: string): Promise { + // Skip API discovery in test environment + if (process.env.NODE_ENV === "test" || process.env.VITEST) { + return MORPHEUS_MODEL_CATALOG.map(buildMorpheusModelDefinition); + } + + // If no API key provided, use static catalog + if (!apiKey?.trim()) { + return MORPHEUS_MODEL_CATALOG.map(buildMorpheusModelDefinition); + } + + try { + const response = await fetch(`${MORPHEUS_BASE_URL}/models`, { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + signal: AbortSignal.timeout(5000), + }); + + if (!response.ok) { + console.warn( + `[morpheus-models] Failed to discover models: HTTP ${response.status}, using static catalog`, + ); + return MORPHEUS_MODEL_CATALOG.map(buildMorpheusModelDefinition); + } + + const data = (await response.json()) as MorpheusModelsResponse; + if (!Array.isArray(data.data) || data.data.length === 0) { + console.warn("[morpheus-models] No models found from API, using static catalog"); + return MORPHEUS_MODEL_CATALOG.map(buildMorpheusModelDefinition); + } + + // Filter to LLM models only and merge with catalog metadata + const llmModels = data.data.filter((m) => m.modelType === "LLM"); + const catalogById = new Map( + MORPHEUS_MODEL_CATALOG.map((m) => [m.id, m]), + ); + const models: ModelDefinitionConfig[] = []; + + for (const apiModel of llmModels) { + const catalogEntry = catalogById.get(apiModel.id); + if (catalogEntry) { + // Use catalog metadata for known models + models.push(buildMorpheusModelDefinition(catalogEntry)); + } else { + // Create definition for newly discovered models not in catalog + const inferred = inferModelProperties(apiModel); + const displayName = apiModel.id + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); + + models.push({ + id: apiModel.id, + name: displayName, + reasoning: inferred.reasoning, + input: inferred.input as ("text" | "image")[], + cost: MORPHEUS_DEFAULT_COST, + contextWindow: inferred.contextWindow, + maxTokens: 8192, + }); + } + } + + return models.length > 0 ? models : MORPHEUS_MODEL_CATALOG.map(buildMorpheusModelDefinition); + } catch (error) { + console.warn(`[morpheus-models] Discovery failed: ${String(error)}, using static catalog`); + return MORPHEUS_MODEL_CATALOG.map(buildMorpheusModelDefinition); + } +} diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 6b49ff17b..d06cf39ea 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -20,6 +20,7 @@ export type AuthChoiceGroupId = | "minimax" | "synthetic" | "venice" + | "morpheus" | "qwen"; export type AuthChoiceGroup = { @@ -71,6 +72,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "Privacy-focused (uncensored models)", choices: ["venice-api-key"], }, + { + value: "morpheus", + label: "Morpheus", + hint: "Decentralized inference (FREE beta)", + choices: ["morpheus-api-key"], + }, { value: "google", label: "Google", @@ -147,6 +154,11 @@ export function buildAuthChoiceOptions(params: { label: "Venice AI API key", hint: "Privacy-focused inference (uncensored models)", }); + options.push({ + value: "morpheus-api-key", + label: "Morpheus API key", + hint: "Decentralized inference (FREE during beta)", + }); options.push({ value: "github-copilot", label: "GitHub Copilot (GitHub device login)", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..3f9cc29fb 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -25,6 +25,8 @@ import { applySyntheticProviderConfig, applyVeniceConfig, applyVeniceProviderConfig, + applyMorpheusConfig, + applyMorpheusProviderConfig, applyVercelAiGatewayConfig, applyVercelAiGatewayProviderConfig, applyZaiConfig, @@ -33,6 +35,7 @@ import { OPENROUTER_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF, VENICE_DEFAULT_MODEL_REF, + MORPHEUS_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, setGeminiApiKey, setKimiCodeApiKey, @@ -41,6 +44,7 @@ import { setOpenrouterApiKey, setSyntheticApiKey, setVeniceApiKey, + setMorpheusApiKey, setVercelAiGatewayApiKey, setZaiApiKey, ZAI_DEFAULT_MODEL_REF, @@ -83,6 +87,8 @@ export async function applyAuthChoiceApiProviders( authChoice = "synthetic-api-key"; } else if (params.opts.tokenProvider === "venice") { authChoice = "venice-api-key"; + } else if (params.opts.tokenProvider === "morpheus") { + authChoice = "morpheus-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; } @@ -522,6 +528,65 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "morpheus-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "morpheus") { + await setMorpheusApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "Morpheus provides decentralized AI inference via the Morpheus Network.", + "Get your API key at: https://app.mor.org", + "Currently FREE during Open Beta (until 1/31/26).", + ].join("\n"), + "Morpheus Inference", + ); + } + + const envKey = resolveEnvApiKey("morpheus"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing MORPHEUS_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setMorpheusApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Morpheus API key", + validate: validateApiKeyInput, + }); + await setMorpheusApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "morpheus:default", + provider: "morpheus", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: MORPHEUS_DEFAULT_MODEL_REF, + applyDefaultConfig: applyMorpheusConfig, + applyProviderConfig: applyMorpheusProviderConfig, + noteDefault: MORPHEUS_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + if (authChoice === "opencode-zen") { let hasCredential = false; if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "opencode") { diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 921ee01d1..c4fb597cb 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -10,6 +10,12 @@ import { VENICE_DEFAULT_MODEL_REF, VENICE_MODEL_CATALOG, } from "../agents/venice-models.js"; +import { + buildMorpheusModelDefinition, + MORPHEUS_BASE_URL, + MORPHEUS_DEFAULT_MODEL_REF, + MORPHEUS_MODEL_CATALOG, +} from "../agents/morpheus-models.js"; import type { MoltbotConfig } from "../config/config.js"; import { OPENROUTER_DEFAULT_MODEL_REF, @@ -411,6 +417,75 @@ export function applyVeniceConfig(cfg: MoltbotConfig): MoltbotConfig { }; } +export function applyMorpheusProviderConfig(cfg: MoltbotConfig): MoltbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[MORPHEUS_DEFAULT_MODEL_REF] = { + ...models[MORPHEUS_DEFAULT_MODEL_REF], + alias: models[MORPHEUS_DEFAULT_MODEL_REF]?.alias ?? "Llama 3.3 70B", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.morpheus; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const morpheusModels = MORPHEUS_MODEL_CATALOG.map(buildMorpheusModelDefinition); + const mergedModels = [ + ...existingModels, + ...morpheusModels.filter( + (model) => !existingModels.some((existing) => existing.id === model.id), + ), + ]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.morpheus = { + ...existingProviderRest, + baseUrl: MORPHEUS_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : morpheusModels, + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +export function applyMorpheusConfig(cfg: MoltbotConfig): MoltbotConfig { + const next = applyMorpheusProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: MORPHEUS_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyAuthProfileConfig( cfg: MoltbotConfig, params: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index b2fb58542..de13efda7 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -100,7 +100,6 @@ export async function setSyntheticApiKey(key: string, agentDir?: string) { } export async function setVeniceApiKey(key: string, agentDir?: string) { - // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ profileId: "venice:default", credential: { @@ -112,6 +111,20 @@ export async function setVeniceApiKey(key: string, agentDir?: string) { }); } +export async function setMorpheusApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "morpheus:default", + credential: { + type: "api_key", + provider: "morpheus", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + +export const MORPHEUS_DEFAULT_MODEL_REF = "morpheus/llama-3.3-70b"; + export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7"; export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto"; export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.5"; diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..b4963a5f1 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -3,6 +3,10 @@ export { SYNTHETIC_DEFAULT_MODEL_REF, } from "../agents/synthetic-models.js"; export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js"; +export { + MORPHEUS_DEFAULT_MODEL_ID, + MORPHEUS_DEFAULT_MODEL_REF, +} from "../agents/morpheus-models.js"; export { applyAuthProfileConfig, applyKimiCodeConfig, @@ -15,6 +19,8 @@ export { applySyntheticProviderConfig, applyVeniceConfig, applyVeniceProviderConfig, + applyMorpheusConfig, + applyMorpheusProviderConfig, applyVercelAiGatewayConfig, applyVercelAiGatewayProviderConfig, applyZaiConfig, @@ -43,6 +49,7 @@ export { setOpenrouterApiKey, setSyntheticApiKey, setVeniceApiKey, + setMorpheusApiKey, setVercelAiGatewayApiKey, setZaiApiKey, writeOAuthCredentials, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..51c0ff7b2 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -17,6 +17,7 @@ export type AuthChoice = | "kimi-code-api-key" | "synthetic-api-key" | "venice-api-key" + | "morpheus-api-key" | "codex-cli" | "apiKey" | "gemini-api-key"