diff --git a/docs/providers/index.md b/docs/providers/index.md index c18ad70fb..11809b213 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) +- [Maple AI (TEE-based private inference)](/providers/maple) - [Ollama (local models)](/providers/ollama) ## Transcription providers diff --git a/docs/providers/maple.md b/docs/providers/maple.md new file mode 100644 index 000000000..149485383 --- /dev/null +++ b/docs/providers/maple.md @@ -0,0 +1,204 @@ +# Maple AI Provider + +Maple AI provides TEE-based (Trusted Execution Environment) private inference using Confidential Computing. All inference runs in secure enclaves with end-to-end encryption and cryptographic attestations, ensuring your prompts and responses remain private. + +## How It Works + +Maple AI runs as a local proxy (desktop app or Docker container) that connects to secure TEE enclaves. Your data is encrypted end-to-end and never visible to Maple or any third party. Maple runs the largest, state-of-the-art open models and does not share any data back to the model creators. Sign up at [trymaple.ai](https://trymaple.ai) to get started. Maple Proxy requires a paid account with API credits. + +1. **Desktop App or Docker**: Run the Maple proxy locally +2. **Local Proxy**: Default endpoint at `http://127.0.0.1:8080/v1` +3. **TEE Backend**: Requests are forwarded to Maple's secure enclaves +4. **Cryptographic Attestation**: Verify the enclave is running trusted code + +## Features + +- **End-to-end encryption**: Your prompts and responses are encrypted +- **Cryptographic attestations**: Verify the secure enclave integrity +- **Open-source verifiable code**: Audit the code running in the enclave +- **OpenAI-compatible API**: Standard `/v1` endpoints for easy integration +- **Streaming**: Required for all completions + +## Setup + +### 1. Install Maple Proxy + +**Desktop App (Recommended)** + +Download and run the Maple desktop app from [trymaple.ai/downloads](https://trymaple.ai/downloads). The proxy runs automatically on `http://127.0.0.1:8080/v1`. + +**Docker** + +```bash +docker run -p 8080:8080 \ + -e MAPLE_BACKEND_URL=https://enclave.trymaple.ai \ + -e MAPLE_ENABLE_CORS=true \ + trymaple/proxy +``` + +### 2. Generate API Key + +Open the Maple app and generate an API key. This key authenticates your requests to the local proxy. + +### 3. Configure Moltbot + +**Option A: Interactive Setup (Recommended)** + +```bash +moltbot onboard --auth-choice maple-api-key +``` + +This will: +1. Prompt for your API key +2. Ask for the proxy URL (defaults to `http://127.0.0.1:8080/v1`) +3. Configure the provider automatically + +**Option B: Environment Variable** + +```bash +export MAPLE_API_KEY="your-api-key" +``` + +**Option C: Non-interactive** + +```bash +moltbot onboard --non-interactive \ + --auth-choice maple-api-key \ + --token "your-api-key" \ + --token-provider maple +``` + +### 4. Verify Setup + +```bash +moltbot chat --model maple/llama-3.3-70b "Hello, are you working?" +``` + +## Available Models + +| Model ID | Name | Use Case | Pricing | +|----------|------|----------|---------| +| `kimi-k2-thinking` | Kimi K2 Thinking | Complex agentic workflows, multi-step coding, web research | $4/$4 per M tokens | +| `gpt-oss-120b` | GPT OSS 120B | Creative writing, structured data | $4/$4 | +| `deepseek-r1-0528` | DeepSeek R1 | Research, advanced math, coding | $4/$4 | +| `qwen3-coder-480b` | Qwen3 Coder 480B | Agentic coding, large codebase analysis, browser automation | $4/$4 | +| `qwen3-vl-30b` | Qwen3 VL 30B | Image and video analysis, screenshot-to-code, OCR, GUI automation | $4/$4 | +| `llama-3.3-70b` | Llama 3.3 70B | General reasoning, conversation | $4/$4 | +| `gemma-3-27b` | Gemma 3 27B | General purpose, efficient | $10/$10 | + +## Model Selection + +Change your default model anytime: + +```bash +moltbot models set maple/llama-3.3-70b +moltbot models set maple/deepseek-r1-0528 +``` + +List available models: + +```bash +moltbot models list | grep maple +``` + +## Configuration + +### Custom Proxy URL + +If running the proxy on a different host or port: + +```yaml +# ~/.moltbot.yaml +models: + providers: + maple: + baseUrl: "http://192.168.1.100:8080/v1" + api: "openai-completions" + apiKey: "MAPLE_API_KEY" +``` + +### Docker Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `MAPLE_BACKEND_URL` | TEE enclave URL | `https://enclave.trymaple.ai` | +| `MAPLE_ENABLE_CORS` | Enable CORS headers | `false` | +| `RUST_LOG` | Log level | `info` | + +## Usage Examples + +```bash +# General chat +moltbot chat --model maple/llama-3.3-70b + +# Advanced reasoning +moltbot chat --model maple/kimi-k2-thinking + +# Research and coding +moltbot chat --model maple/deepseek-r1-0528 + +# Vision tasks +moltbot chat --model maple/qwen3-vl-30b + +# Coding tasks +moltbot chat --model maple/qwen3-coder-480b +``` + +## Privacy and Security + +### Why TEE? + +Trusted Execution Environments (TEEs) provide hardware-level isolation: + +- **Memory encryption**: Data is encrypted in memory +- **Attestation**: Cryptographic proof of what code is running +- **Isolation**: Even the host system cannot access enclave data + +### Security Proof Attestation + +Maple provides cryptographic attestations that prove the integrity of the secure enclave. You can view the current attestations at [trymaple.ai/proof](https://trymaple.ai/proof). + +The Maple Proxy automatically verifies these attestations before connecting to the backend. If the attestation is invalid or tampered with, the proxy refuses to connect, similar to how SSL/TLS certificates protect web connections. This ensures you're always communicating with a genuine, unmodified Maple enclave. + +### Verification + +You can verify the enclave attestation to ensure: + +1. The code running matches the open-source release +2. The TEE hardware is genuine +3. No tampering has occurred + +Visit [trymaple.ai/proof](https://trymaple.ai/proof) to inspect the current attestation details. + +## Troubleshooting + +### Proxy not running + +Ensure the Maple app is running or Docker container is active: + +```bash +curl http://127.0.0.1:8080/health +``` + +### Connection refused + +Check the proxy URL is correct and the service is running: + +```bash +# Test connectivity +curl -H "Authorization: Bearer $MAPLE_API_KEY" \ + http://127.0.0.1:8080/v1/models +``` + +### Model not available + +The model list is fetched from the proxy. Ensure your Maple subscription includes the model you're trying to use. + +## Links + +- [Maple AI](https://trymaple.ai) +- [Downloads](https://trymaple.ai/downloads) +- [Security Proof](https://trymaple.ai/proof) +- [Proxy Documentation](https://blog.trymaple.ai/maple-proxy-documentation/) +- [Maple GitHub](https://github.com/OpenSecretCloud/Maple) +- [Maple Proxy GitHub](https://github.com/OpenSecretCloud/maple-proxy) diff --git a/src/agents/maple-models.ts b/src/agents/maple-models.ts new file mode 100644 index 000000000..8d1e48fa8 --- /dev/null +++ b/src/agents/maple-models.ts @@ -0,0 +1,212 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; + +/** + * Maple AI Provider + * + * Maple AI is a privacy-focused AI provider that uses Confidential Computing (TEEs) + * to provide end-to-end encryption with cryptographic attestations. Users run the + * Maple desktop app or Docker container, then point their tools at the local proxy. + * + * Default proxy URL: http://127.0.0.1:8080/v1 + */ + +export const MAPLE_DEFAULT_BASE_URL = "http://127.0.0.1:8080/v1"; +export const MAPLE_DEFAULT_MODEL_ID = "kimi-k2-thinking"; +export const MAPLE_DEFAULT_MODEL_REF = `maple/${MAPLE_DEFAULT_MODEL_ID}`; + +// Maple uses flat pricing per million tokens +export const MAPLE_DEFAULT_COST = { + input: 4, + output: 4, + cacheRead: 0, + cacheWrite: 0, +}; + +/** + * Static catalog of Maple AI models. + * + * All models run in TEE-based Confidential Computing environments, + * providing end-to-end encryption and cryptographic attestations. + * + * This catalog serves as a fallback when the Maple API is unreachable. + */ +export const MAPLE_MODEL_CATALOG = [ + { + id: "kimi-k2-thinking", + name: "Kimi K2 Thinking", + description: "Complex agentic workflows, multi-step coding, web research", + reasoning: true, + input: ["text"] as const, + contextWindow: 262144, + maxTokens: 8192, + cost: { input: 4, output: 4, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "gpt-oss-120b", + name: "GPT OSS 120B", + description: "Creative writing, structured data", + reasoning: false, + input: ["text"] as const, + contextWindow: 131072, + maxTokens: 8192, + cost: { input: 4, output: 4, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "deepseek-r1-0528", + name: "DeepSeek R1", + description: "Research, advanced math, coding", + reasoning: true, + input: ["text"] as const, + contextWindow: 131072, + maxTokens: 8192, + cost: { input: 4, output: 4, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "qwen3-coder-480b", + name: "Qwen3 Coder 480B", + description: "Agentic coding, large codebase analysis, browser automation", + reasoning: false, + input: ["text"] as const, + contextWindow: 131072, + maxTokens: 8192, + cost: { input: 4, output: 4, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "qwen3-vl-30b", + name: "Qwen3 VL 30B", + description: "Image and video analysis, screenshot-to-code, OCR, GUI automation", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 262144, + maxTokens: 8192, + cost: { input: 4, output: 4, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "llama-3.3-70b", + name: "Llama 3.3 70B", + description: "General reasoning, conversation", + reasoning: false, + input: ["text"] as const, + contextWindow: 131072, + maxTokens: 8192, + cost: { input: 4, output: 4, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "gemma-3-27b", + name: "Gemma 3 27B", + description: "General purpose, efficient", + reasoning: false, + input: ["text"] as const, + contextWindow: 131072, + maxTokens: 8192, + cost: { input: 10, output: 10, cacheRead: 0, cacheWrite: 0 }, + }, +] as const; + +export type MapleCatalogEntry = (typeof MAPLE_MODEL_CATALOG)[number]; + +/** + * Build a ModelDefinitionConfig from a Maple catalog entry. + */ +export function buildMapleModelDefinition(entry: MapleCatalogEntry): ModelDefinitionConfig { + return { + id: entry.id, + name: entry.name, + reasoning: entry.reasoning, + input: [...entry.input], + cost: entry.cost, + contextWindow: entry.contextWindow, + maxTokens: entry.maxTokens, + }; +} + +// Maple API response types (OpenAI-compatible) +interface MapleModel { + id: string; + object: string; + owned_by?: string; +} + +interface MapleModelsResponse { + object: string; + data: MapleModel[]; +} + +/** + * Discover models from Maple API with fallback to static catalog. + * Requires authentication (Bearer token). + */ +export async function discoverMapleModels(params?: { + baseUrl?: string; + apiKey?: string; +}): Promise { + // Skip API discovery in test environment + if (process.env.NODE_ENV === "test" || process.env.VITEST) { + return MAPLE_MODEL_CATALOG.map(buildMapleModelDefinition); + } + + const baseUrl = params?.baseUrl ?? MAPLE_DEFAULT_BASE_URL; + const apiKey = params?.apiKey; + + // If no API key, return static catalog + if (!apiKey) { + return MAPLE_MODEL_CATALOG.map(buildMapleModelDefinition); + } + + try { + const response = await fetch(`${baseUrl}/models`, { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + signal: AbortSignal.timeout(5000), + }); + + if (!response.ok) { + console.warn( + `[maple-models] Failed to discover models: HTTP ${response.status}, using static catalog`, + ); + return MAPLE_MODEL_CATALOG.map(buildMapleModelDefinition); + } + + const data = (await response.json()) as MapleModelsResponse; + if (!Array.isArray(data.data) || data.data.length === 0) { + console.warn("[maple-models] No models found from API, using static catalog"); + return MAPLE_MODEL_CATALOG.map(buildMapleModelDefinition); + } + + // Merge discovered models with catalog metadata + const catalogById = new Map( + MAPLE_MODEL_CATALOG.map((m) => [m.id, m]), + ); + const models: ModelDefinitionConfig[] = []; + + for (const apiModel of data.data) { + const catalogEntry = catalogById.get(apiModel.id); + if (catalogEntry) { + // Use catalog metadata for known models + models.push(buildMapleModelDefinition(catalogEntry)); + } else { + // Create definition for newly discovered models not in catalog + const isReasoning = + apiModel.id.toLowerCase().includes("thinking") || + apiModel.id.toLowerCase().includes("reason") || + apiModel.id.toLowerCase().includes("r1"); + + models.push({ + id: apiModel.id, + name: apiModel.id, + reasoning: isReasoning, + input: ["text"], + cost: MAPLE_DEFAULT_COST, + contextWindow: 128000, + maxTokens: 8192, + }); + } + } + + return models.length > 0 ? models : MAPLE_MODEL_CATALOG.map(buildMapleModelDefinition); + } catch (error) { + console.warn(`[maple-models] Discovery failed: ${String(error)}, using static catalog`); + return MAPLE_MODEL_CATALOG.map(buildMapleModelDefinition); + } +} diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 96e4e4ae6..1dcdfe9c9 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", + maple: "MAPLE_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..5c6212379 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 { discoverMapleModels, MAPLE_DEFAULT_BASE_URL } from "./maple-models.js"; type ModelsConfig = NonNullable; export type ProviderConfig = NonNullable[string]; @@ -350,6 +351,15 @@ async function buildVeniceProvider(): Promise { }; } +async function buildMapleProvider(params?: { apiKey?: string }): Promise { + const models = await discoverMapleModels({ apiKey: params?.apiKey }); + return { + baseUrl: MAPLE_DEFAULT_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 mapleKey = + resolveEnvApiKeyVarName("maple") ?? + resolveApiKeyFromProfiles({ provider: "maple", store: authStore }); + if (mapleKey) { + providers.maple = { ...(await buildMapleProvider({ apiKey: mapleKey })), apiKey: mapleKey }; + } + const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal"); if (qwenProfiles.length > 0) { providers["qwen-portal"] = { diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 6b49ff17b..e56612223 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" + | "maple" | "qwen"; export type AuthChoiceGroup = { @@ -71,6 +72,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "Privacy-focused (uncensored models)", choices: ["venice-api-key"], }, + { + value: "maple", + label: "Maple AI", + hint: "TEE-based private inference", + choices: ["maple-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: "maple-api-key", + label: "Maple AI API key", + hint: "TEE-based private inference (end-to-end encrypted)", + }); 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..3ba507bf6 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, + applyMapleConfig, + applyMapleProviderConfig, applyVercelAiGatewayConfig, applyVercelAiGatewayProviderConfig, applyZaiConfig, @@ -33,6 +35,7 @@ import { OPENROUTER_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF, VENICE_DEFAULT_MODEL_REF, + MAPLE_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, setGeminiApiKey, setKimiCodeApiKey, @@ -41,6 +44,7 @@ import { setOpenrouterApiKey, setSyntheticApiKey, setVeniceApiKey, + setMapleApiKey, 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 === "maple") { + authChoice = "maple-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; } @@ -522,6 +528,78 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "maple-api-key") { + let hasCredential = false; + let baseUrl: string | undefined; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "maple") { + await setMapleApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "Maple AI provides TEE-based private inference with end-to-end encryption.", + "Download the Maple desktop app at: https://trymaple.ai/downloads", + "Run the app or Docker container, then configure the proxy URL.", + "Default URL: http://127.0.0.1:8080/v1", + "Generate your API key within the Maple app.", + ].join("\n"), + "Maple AI", + ); + } + + const envKey = resolveEnvApiKey("maple"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing MAPLE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setMapleApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Maple AI API key", + validate: validateApiKeyInput, + }); + await setMapleApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + + // Ask for base URL (with default) + const customUrl = await params.prompter.text({ + message: "Enter Maple proxy URL (press Enter for default)", + placeholder: "http://127.0.0.1:8080/v1", + }); + if (customUrl && String(customUrl).trim()) { + baseUrl = String(customUrl).trim(); + } + + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "maple:default", + provider: "maple", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: MAPLE_DEFAULT_MODEL_REF, + applyDefaultConfig: (config) => applyMapleConfig(config, { baseUrl }), + applyProviderConfig: (config) => applyMapleProviderConfig(config, { baseUrl }), + noteDefault: MAPLE_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/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 6fe26b59a..8f2e5ad2b 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -20,6 +20,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "zai-api-key": "zai", "synthetic-api-key": "synthetic", "venice-api-key": "venice", + "maple-api-key": "maple", "github-copilot": "github-copilot", "copilot-proxy": "copilot-proxy", "minimax-cloud": "minimax", diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 921ee01d1..bda0fa7e6 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 { + buildMapleModelDefinition, + MAPLE_DEFAULT_BASE_URL, + MAPLE_DEFAULT_MODEL_REF, + MAPLE_MODEL_CATALOG, +} from "../agents/maple-models.js"; import type { MoltbotConfig } from "../config/config.js"; import { OPENROUTER_DEFAULT_MODEL_REF, @@ -411,6 +417,85 @@ export function applyVeniceConfig(cfg: MoltbotConfig): MoltbotConfig { }; } +/** + * Apply Maple provider configuration without changing the default model. + * Registers Maple models and sets up the provider, but preserves existing model selection. + */ +export function applyMapleProviderConfig( + cfg: MoltbotConfig, + params?: { baseUrl?: string }, +): MoltbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[MAPLE_DEFAULT_MODEL_REF] = { + ...models[MAPLE_DEFAULT_MODEL_REF], + alias: models[MAPLE_DEFAULT_MODEL_REF]?.alias ?? "Kimi K2 Thinking", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.maple; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const mapleModels = MAPLE_MODEL_CATALOG.map(buildMapleModelDefinition); + const mergedModels = [ + ...existingModels, + ...mapleModels.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(); + const baseUrl = params?.baseUrl ?? MAPLE_DEFAULT_BASE_URL; + providers.maple = { + ...existingProviderRest, + baseUrl, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : mapleModels, + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +/** + * Apply Maple provider configuration AND set Maple as the default model. + * Use this when Maple is the primary provider choice during onboarding. + */ +export function applyMapleConfig(cfg: MoltbotConfig, params?: { baseUrl?: string }): MoltbotConfig { + const next = applyMapleProviderConfig(cfg, params); + 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: MAPLE_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..15a5a9172 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -112,6 +112,19 @@ export async function setVeniceApiKey(key: string, agentDir?: string) { }); } +export async function setMapleApiKey(key: string, agentDir?: string) { + // Write to resolved agent dir so gateway finds credentials on startup. + upsertAuthProfile({ + profileId: "maple:default", + credential: { + type: "api_key", + provider: "maple", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + 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..5af9842d7 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -3,6 +3,7 @@ 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 { MAPLE_DEFAULT_MODEL_ID, MAPLE_DEFAULT_MODEL_REF } from "../agents/maple-models.js"; export { applyAuthProfileConfig, applyKimiCodeConfig, @@ -15,6 +16,8 @@ export { applySyntheticProviderConfig, applyVeniceConfig, applyVeniceProviderConfig, + applyMapleConfig, + applyMapleProviderConfig, applyVercelAiGatewayConfig, applyVercelAiGatewayProviderConfig, applyZaiConfig, @@ -43,6 +46,7 @@ export { setOpenrouterApiKey, setSyntheticApiKey, setVeniceApiKey, + setMapleApiKey, setVercelAiGatewayApiKey, setZaiApiKey, writeOAuthCredentials, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..ae20c9e9d 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" + | "maple-api-key" | "codex-cli" | "apiKey" | "gemini-api-key" @@ -70,6 +71,8 @@ export type OnboardOptions = { minimaxApiKey?: string; syntheticApiKey?: string; veniceApiKey?: string; + mapleApiKey?: string; + mapleBaseUrl?: string; opencodeZenApiKey?: string; gatewayPort?: number; gatewayBind?: GatewayBind;