fix: fix server terminology with mode aliases (#277)

This commit is contained in:
Charles Packer
2026-02-10 20:34:29 -08:00
committed by GitHub
parent 83569d968e
commit de1adcf4fe
21 changed files with 198 additions and 133 deletions

View File

@@ -386,7 +386,7 @@ Check the [ADE](https://app.letta.com) to see if your agent is attempting to use
## Documentation ## Documentation
- [Getting Started](docs/getting-started.md) - [Getting Started](docs/getting-started.md)
- [Self-Hosted Setup](docs/selfhosted-setup.md) - Run with your own Letta server - [Docker Server Setup](docs/selfhosted-setup.md) - Run with your own Letta server
- [Configuration Reference](docs/configuration.md) - [Configuration Reference](docs/configuration.md)
- [Slack Setup](docs/slack-setup.md) - [Slack Setup](docs/slack-setup.md)
- [Discord Setup](docs/discord-setup.md) - [Discord Setup](docs/discord-setup.md)

View File

@@ -61,7 +61,7 @@ The wizard will guide you through:
| Variable | Description | Default | | Variable | Description | Default |
|----------|-------------|---------| |----------|-------------|---------|
| `LETTA_API_KEY` | API key from app.letta.com | Required (unless self-hosted) | | `LETTA_API_KEY` | API key from app.letta.com | Required (unless using a Docker server) |
| `LETTA_BASE_URL` | API endpoint | `https://api.letta.com` | | `LETTA_BASE_URL` | API endpoint | `https://api.letta.com` |
### Agent Selection ### Agent Selection
@@ -329,9 +329,9 @@ The agent can verify success by checking:
- Config file exists at `~/.lettabot/config.yaml` - Config file exists at `~/.lettabot/config.yaml`
- User can message bot on configured channel(s) - User can message bot on configured channel(s)
## Self-Hosted Letta ## Docker Server Letta
To use a self-hosted Letta server: To use a Letta Docker server:
```bash ```bash
# Run Letta Docker # Run Letta Docker

View File

@@ -58,7 +58,7 @@ describe('myFunction', () => {
## E2E Tests ## E2E Tests
E2E tests verify the full message flow against a real Letta Cloud agent. E2E tests verify the full message flow against a real Letta API agent.
### Setup ### Setup
@@ -73,7 +73,7 @@ Without these, E2E tests are automatically skipped.
### Test Agent ### Test Agent
We use a dedicated test agent named "greg" on Letta Cloud. This agent: We use a dedicated test agent named "greg" on Letta API. This agent:
- Has minimal configuration - Has minimal configuration
- Is only used for automated testing - Is only used for automated testing
- Should not have any sensitive data - Should not have any sensitive data
@@ -137,7 +137,7 @@ Tests run automatically via GitHub Actions (`.github/workflows/test.yml`):
| Job | Trigger | What it tests | | Job | Trigger | What it tests |
|-----|---------|---------------| |-----|---------|---------------|
| `unit` | All PRs and pushes | Unit tests only | | `unit` | All PRs and pushes | Unit tests only |
| `e2e` | Pushes to main | Full E2E with Letta Cloud | | `e2e` | Pushes to main | Full E2E with Letta API |
E2E tests only run on `main` because they require secrets that aren't available to fork PRs. E2E tests only run on `main` because they require secrets that aren't available to fork PRs.

View File

@@ -5,7 +5,7 @@ LettaBot is a multi-channel AI assistant powered by [Letta](https://letta.com) t
## Guides ## Guides
- [Getting Started](./getting-started.md) - Installation and basic setup - [Getting Started](./getting-started.md) - Installation and basic setup
- [Self-Hosted Setup](./selfhosted-setup.md) - Run with your own Letta server - [Docker Server Setup](./selfhosted-setup.md) - Run with your own Letta server
- [Configuration Reference](./configuration.md) - All config options - [Configuration Reference](./configuration.md) - All config options
- [Commands Reference](./commands.md) - Bot commands reference - [Commands Reference](./commands.md) - Bot commands reference
- [CLI Tools](./cli-tools.md) - Agent/operator CLI tools - [CLI Tools](./cli-tools.md) - Agent/operator CLI tools
@@ -62,7 +62,7 @@ LettaBot uses a **single agent with unified memory** across all channels:
┌──────────────────────────┐ ┌──────────────────────────┐
│ Letta Server │ │ Letta Server │
│ (api.letta.com or │ │ (api.letta.com or │
self-hosted Docker) Docker/custom)
│ │ │ │
│ • Agent Memory │ │ • Agent Memory │
│ • LLM Inference │ │ • LLM Inference │

View File

@@ -21,8 +21,8 @@ For global installs (`npm install -g`), either:
```yaml ```yaml
# Server connection # Server connection
server: server:
mode: cloud # 'cloud' or 'selfhosted' mode: api # 'api' or 'docker' (legacy: 'cloud'/'selfhosted')
apiKey: letta_... # Required for cloud mode apiKey: letta_... # Required for api mode
# Agent settings (single agent mode) # Agent settings (single agent mode)
# For multiple agents, use `agents:` array instead -- see Multi-Agent section # For multiple agents, use `agents:` array instead -- see Multi-Agent section
@@ -98,15 +98,15 @@ api:
| Option | Type | Description | | Option | Type | Description |
|--------|------|-------------| |--------|------|-------------|
| `server.mode` | `'cloud'` \| `'selfhosted'` | Connection mode | | `server.mode` | `'api'` \| `'docker'` | Connection mode (legacy aliases: `'cloud'`, `'selfhosted'`) |
| `server.apiKey` | string | API key for Letta Cloud | | `server.apiKey` | string | API key for Letta API |
| `server.baseUrl` | string | URL for self-hosted server (e.g., `http://localhost:8283`) | | `server.baseUrl` | string | URL for Docker/custom server (e.g., `http://localhost:8283`) |
### Self-Hosted Mode ### Docker Server Mode
```yaml ```yaml
server: server:
mode: selfhosted mode: docker
baseUrl: http://localhost:8283 baseUrl: http://localhost:8283
``` ```
@@ -142,7 +142,7 @@ Use the `agents:` array instead of the top-level `agent:` and `channels:` keys:
```yaml ```yaml
server: server:
mode: cloud mode: api
apiKey: letta_... apiKey: letta_...
agents: agents:

View File

@@ -17,7 +17,7 @@ Deploy LettaBot to [Railway](https://railway.app) for always-on hosting.
| Variable | Description | | Variable | Description |
|----------|-------------| |----------|-------------|
| `LETTA_API_KEY` | Your Letta Cloud API key ([get one here](https://app.letta.com)) | | `LETTA_API_KEY` | Your Letta API key ([get one here](https://app.letta.com)) |
### Channel Configuration (at least one required) ### Channel Configuration (at least one required)
@@ -59,7 +59,7 @@ SLACK_APP_TOKEN=xapp-...
On startup, LettaBot: On startup, LettaBot:
1. Checks for `LETTA_AGENT_ID` env var - uses if set 1. Checks for `LETTA_AGENT_ID` env var - uses if set
2. Otherwise, searches Letta Cloud for an agent named `LETTA_AGENT_NAME` (or legacy `AGENT_NAME`, default: "LettaBot") 2. Otherwise, searches Letta API for an agent named `LETTA_AGENT_NAME` (or legacy `AGENT_NAME`, default: "LettaBot")
3. If found, uses the existing agent (preserves memory!) 3. If found, uses the existing agent (preserves memory!)
4. If not found, creates a new agent on first message 4. If not found, creates a new agent on first message

View File

@@ -1,6 +1,6 @@
# Self-Hosted Letta Server Setup # Docker Server Setup
Run LettaBot with your own Letta server instead of Letta Cloud. Run LettaBot with your own Letta Docker/custom server instead of Letta API.
## Prerequisites ## Prerequisites
@@ -42,7 +42,7 @@ curl http://localhost:8283/v1/health
lettabot onboard lettabot onboard
``` ```
Select "Enter self-hosted URL" and enter `http://localhost:8283`. Select "Enter Docker server URL" and enter `http://localhost:8283`.
### Option B: Manual Configuration ### Option B: Manual Configuration
@@ -50,7 +50,7 @@ Create `lettabot.yaml`:
```yaml ```yaml
server: server:
mode: selfhosted mode: docker
baseUrl: http://localhost:8283 baseUrl: http://localhost:8283
# apiKey: optional-if-server-requires-auth # apiKey: optional-if-server-requires-auth
@@ -79,7 +79,7 @@ lettabot server
You should see: You should see:
``` ```
[Config] Loaded from /path/to/lettabot.yaml [Config] Loaded from /path/to/lettabot.yaml
[Config] Mode: selfhosted, Agent: LettaBot, Model: gpt-4o [Config] Mode: docker, Agent: LettaBot, Model: gpt-4o
Starting LettaBot... Starting LettaBot...
LettaBot initialized. Agent ID: (new) LettaBot initialized. Agent ID: (new)
[Telegram] Bot started as @YourBotName [Telegram] Bot started as @YourBotName

View File

@@ -1,7 +1,7 @@
/** /**
* E2E Tests for LettaBot * E2E Tests for LettaBot
* *
* These tests use a real Letta Cloud agent to verify the full message flow. * These tests use a real Letta API agent to verify the full message flow.
* Requires LETTA_API_KEY and LETTA_E2E_AGENT_ID environment variables. * Requires LETTA_API_KEY and LETTA_E2E_AGENT_ID environment variables.
* *
* Run with: npm run test:e2e * Run with: npm run test:e2e
@@ -17,7 +17,7 @@ import { join } from 'node:path';
// Skip if no API key (local dev without secrets) // Skip if no API key (local dev without secrets)
const SKIP_E2E = !process.env.LETTA_API_KEY || !process.env.LETTA_E2E_AGENT_ID; const SKIP_E2E = !process.env.LETTA_API_KEY || !process.env.LETTA_E2E_AGENT_ID;
describe.skipIf(SKIP_E2E)('e2e: LettaBot with Letta Cloud', () => { describe.skipIf(SKIP_E2E)('e2e: LettaBot with Letta API', () => {
let bot: LettaBot; let bot: LettaBot;
let mockAdapter: MockChannelAdapter; let mockAdapter: MockChannelAdapter;
let tempDir: string; let tempDir: string;

View File

@@ -1,7 +1,7 @@
/** /**
* E2E Tests for Model API * E2E Tests for Model API
* *
* Tests model listing and retrieval against Letta Cloud. * Tests model listing and retrieval against Letta API.
* Requires LETTA_API_KEY and LETTA_E2E_AGENT_ID environment variables. * Requires LETTA_API_KEY and LETTA_E2E_AGENT_ID environment variables.
* *
* Run with: npm run test:e2e * Run with: npm run test:e2e
@@ -13,10 +13,10 @@ import { listModels, getAgentModel } from '../src/tools/letta-api.js';
const SKIP_E2E = !process.env.LETTA_API_KEY || !process.env.LETTA_E2E_AGENT_ID; const SKIP_E2E = !process.env.LETTA_API_KEY || !process.env.LETTA_E2E_AGENT_ID;
describe.skipIf(SKIP_E2E)('e2e: Model API', () => { describe.skipIf(SKIP_E2E)('e2e: Model API', () => {
it('lists available models from Letta Cloud', async () => { it('lists available models from Letta API', async () => {
const models = await listModels(); const models = await listModels();
expect(models.length).toBeGreaterThan(0); expect(models.length).toBeGreaterThan(0);
// Known providers should always exist on Letta Cloud // Known providers should always exist on Letta API
const handles = models.map(m => m.handle); const handles = models.map(m => m.handle);
expect(handles.some(h => h.includes('anthropic') || h.includes('openai'))).toBe(true); expect(handles.some(h => h.includes('anthropic') || h.includes('openai'))).toBe(true);
}, 30000); }, 30000);

View File

@@ -2,15 +2,16 @@
# Copy this to lettabot.yaml and fill in your values. # Copy this to lettabot.yaml and fill in your values.
# #
# Server modes: # Server modes:
# - 'cloud': Use Letta Cloud (api.letta.com) with API key # - 'api': Use Letta API (api.letta.com) with API key
# - 'selfhosted': Use self-hosted Letta server # - 'docker': Use a Docker/custom Letta server
# Legacy aliases still accepted: 'cloud', 'selfhosted'
server: server:
mode: cloud mode: api
# For cloud mode, set your API key (get one at https://app.letta.com): # For api mode, set your API key (get one at https://app.letta.com):
apiKey: sk-let-YOUR-API-KEY apiKey: sk-let-YOUR-API-KEY
# For selfhosted mode, uncomment and set the base URL: # For docker mode, uncomment and set the base URL:
# mode: selfhosted # mode: docker
# baseUrl: http://localhost:8283 # baseUrl: http://localhost:8283
agent: agent:
@@ -19,8 +20,8 @@ agent:
# Note: model is configured on the Letta agent server-side. # Note: model is configured on the Letta agent server-side.
# Select a model during `lettabot onboard` or change it with `lettabot model set <handle>`. # Select a model during `lettabot onboard` or change it with `lettabot model set <handle>`.
# BYOK Providers (optional, cloud mode only) # BYOK Providers (optional, api mode only)
# These will be synced to Letta Cloud on startup # These will be synced to Letta API on startup
# providers: # providers:
# - id: anthropic # - id: anthropic
# name: lc-anthropic # name: lc-anthropic

View File

@@ -1,5 +1,5 @@
/** /**
* OAuth 2.0 utilities for Letta Cloud authentication * OAuth 2.0 utilities for Letta API authentication
* Uses Device Code Flow for CLI authentication * Uses Device Code Flow for CLI authentication
* *
* Ported from @letta-ai/letta-code * Ported from @letta-ai/letta-code
@@ -7,13 +7,15 @@
import Letta from "@letta-ai/letta-client"; import Letta from "@letta-ai/letta-client";
export const LETTA_CLOUD_API_URL = "https://api.letta.com"; export const LETTA_API_URL = "https://api.letta.com";
// Backward-compatible alias for older imports.
export const LETTA_CLOUD_API_URL = LETTA_API_URL;
export const OAUTH_CONFIG = { export const OAUTH_CONFIG = {
clientId: "ci-let-724dea7e98f4af6f8f370f4b1466200c", clientId: "ci-let-724dea7e98f4af6f8f370f4b1466200c",
clientSecret: "", // Not needed for device code flow clientSecret: "", // Not needed for device code flow
authBaseUrl: "https://app.letta.com", authBaseUrl: "https://app.letta.com",
apiBaseUrl: LETTA_CLOUD_API_URL, apiBaseUrl: LETTA_API_URL,
} as const; } as const;
export interface DeviceCodeResponse { export interface DeviceCodeResponse {

View File

@@ -9,7 +9,7 @@
*/ */
// Config loaded from lettabot.yaml // Config loaded from lettabot.yaml
import { loadConfig, applyConfigToEnv } from './config/index.js'; import { loadConfig, applyConfigToEnv, serverModeLabel } from './config/index.js';
const config = loadConfig(); const config = loadConfig();
applyConfigToEnv(config); applyConfigToEnv(config);
import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { existsSync, readFileSync, writeFileSync } from 'node:fs';
@@ -48,7 +48,7 @@ async function configure() {
// Show current config from YAML // Show current config from YAML
const configRows = [ const configRows = [
['Server Mode', config.server.mode], ['Server Mode', serverModeLabel(config.server.mode)],
['API Key', config.server.apiKey ? '✓ Set' : '✗ Not set'], ['API Key', config.server.apiKey ? '✓ Set' : '✗ Not set'],
['Agent Name', config.agent.name], ['Agent Name', config.agent.name],
['Telegram', config.channels.telegram?.enabled ? '✓ Enabled' : '✗ Disabled'], ['Telegram', config.channels.telegram?.enabled ? '✓ Enabled' : '✗ Disabled'],

View File

@@ -9,7 +9,7 @@
import { getAgentModel, updateAgentModel } from '../tools/letta-api.js'; import { getAgentModel, updateAgentModel } from '../tools/letta-api.js';
import { buildModelOptions, handleModelSelection, getBillingTier } from '../utils/model-selection.js'; import { buildModelOptions, handleModelSelection, getBillingTier } from '../utils/model-selection.js';
import { isLettaCloudUrl } from '../utils/server.js'; import { isLettaApiUrl } from '../utils/server.js';
import { Store } from '../core/store.js'; import { Store } from '../core/store.js';
/** /**
@@ -79,11 +79,11 @@ export async function modelInteractive(): Promise<void> {
p.log.info(`Current model: ${currentModel}`); p.log.info(`Current model: ${currentModel}`);
} }
// Determine if self-hosted // Determine if using Docker/custom server
const baseUrl = process.env.LETTA_BASE_URL; const baseUrl = process.env.LETTA_BASE_URL;
const isSelfHosted = !!baseUrl && !isLettaCloudUrl(baseUrl); const isSelfHosted = !!baseUrl && !isLettaApiUrl(baseUrl);
// Get billing tier for cloud users // Get billing tier for Letta API users
let billingTier: string | null = null; let billingTier: string | null = null;
if (!isSelfHosted) { if (!isSelfHosted) {
const spinner = p.spinner(); const spinner = p.spinner();

View File

@@ -9,7 +9,8 @@ import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path'; import { dirname, join, resolve } from 'node:path';
import YAML from 'yaml'; import YAML from 'yaml';
import type { LettaBotConfig, ProviderConfig } from './types.js'; import type { LettaBotConfig, ProviderConfig } from './types.js';
import { DEFAULT_CONFIG } from './types.js'; import { DEFAULT_CONFIG, canonicalizeServerMode, isApiServerMode, isDockerServerMode } from './types.js';
import { LETTA_API_URL } from '../auth/oauth.js';
// Config file locations (checked in order) // Config file locations (checked in order)
const CONFIG_PATHS = [ const CONFIG_PATHS = [
@@ -64,14 +65,22 @@ export function loadConfig(): LettaBotConfig {
// Re-extract from document AST to preserve the original string representation. // Re-extract from document AST to preserve the original string representation.
fixLargeGroupIds(content, parsed); fixLargeGroupIds(content, parsed);
// Merge with defaults // Merge with defaults and canonicalize server mode.
return { const merged = {
...DEFAULT_CONFIG, ...DEFAULT_CONFIG,
...parsed, ...parsed,
server: { ...DEFAULT_CONFIG.server, ...parsed.server }, server: { ...DEFAULT_CONFIG.server, ...parsed.server },
agent: { ...DEFAULT_CONFIG.agent, ...parsed.agent }, agent: { ...DEFAULT_CONFIG.agent, ...parsed.agent },
channels: { ...DEFAULT_CONFIG.channels, ...parsed.channels }, channels: { ...DEFAULT_CONFIG.channels, ...parsed.channels },
}; };
return {
...merged,
server: {
...merged.server,
mode: canonicalizeServerMode(merged.server.mode),
},
};
} catch (err) { } catch (err) {
console.error(`[Config] Failed to load ${configPath}:`, err); console.error(`[Config] Failed to load ${configPath}:`, err);
return { ...DEFAULT_CONFIG }; return { ...DEFAULT_CONFIG };
@@ -107,7 +116,7 @@ export function configToEnv(config: LettaBotConfig): Record<string, string> {
const env: Record<string, string> = {}; const env: Record<string, string> = {};
// Server // Server
if (config.server.mode === 'selfhosted' && config.server.baseUrl) { if (isDockerServerMode(config.server.mode) && config.server.baseUrl) {
env.LETTA_BASE_URL = config.server.baseUrl; env.LETTA_BASE_URL = config.server.baseUrl;
} }
if (config.server.apiKey) { if (config.server.apiKey) {
@@ -309,10 +318,10 @@ export function applyConfigToEnv(config: LettaBotConfig): void {
} }
/** /**
* Create BYOK providers on Letta Cloud * Create BYOK providers on Letta API
*/ */
export async function syncProviders(config: Partial<LettaBotConfig> & Pick<LettaBotConfig, 'server'>): Promise<void> { export async function syncProviders(config: Partial<LettaBotConfig> & Pick<LettaBotConfig, 'server'>): Promise<void> {
if (config.server.mode !== 'cloud' || !config.server.apiKey) { if (!isApiServerMode(config.server.mode) || !config.server.apiKey) {
return; return;
} }
@@ -321,7 +330,7 @@ export async function syncProviders(config: Partial<LettaBotConfig> & Pick<Letta
} }
const apiKey = config.server.apiKey; const apiKey = config.server.apiKey;
const baseUrl = 'https://api.letta.com'; const baseUrl = LETTA_API_URL;
// List existing providers // List existing providers
const listResponse = await fetch(`${baseUrl}/v1/providers`, { const listResponse = await fetch(`${baseUrl}/v1/providers`, {

View File

@@ -1,7 +1,23 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { normalizeAgents, type LettaBotConfig, type AgentConfig } from './types.js'; import {
normalizeAgents,
canonicalizeServerMode,
isApiServerMode,
isDockerServerMode,
type LettaBotConfig,
type AgentConfig,
} from './types.js';
describe('normalizeAgents', () => { describe('normalizeAgents', () => {
it('canonicalizes legacy server mode aliases', () => {
expect(canonicalizeServerMode('cloud')).toBe('api');
expect(canonicalizeServerMode('api')).toBe('api');
expect(canonicalizeServerMode('selfhosted')).toBe('docker');
expect(canonicalizeServerMode('docker')).toBe('docker');
expect(isApiServerMode('cloud')).toBe(true);
expect(isDockerServerMode('selfhosted')).toBe(true);
});
it('should normalize legacy single-agent config to one-entry array', () => { it('should normalize legacy single-agent config to one-entry array', () => {
const config: LettaBotConfig = { const config: LettaBotConfig = {
server: { mode: 'cloud' }, server: { mode: 'cloud' },

View File

@@ -2,10 +2,29 @@
* LettaBot Configuration Types * LettaBot Configuration Types
* *
* Two modes: * Two modes:
* 1. Self-hosted: Uses baseUrl (e.g., http://localhost:8283), no API key * 1. Docker server: Uses baseUrl (e.g., http://localhost:8283), no API key
* 2. Letta Cloud: Uses apiKey, optional BYOK providers * 2. Letta API: Uses apiKey, optional BYOK providers
*/ */
export type ServerMode = 'api' | 'docker' | 'cloud' | 'selfhosted';
export type CanonicalServerMode = 'api' | 'docker';
export function canonicalizeServerMode(mode?: ServerMode): CanonicalServerMode {
return mode === 'docker' || mode === 'selfhosted' ? 'docker' : 'api';
}
export function isDockerServerMode(mode?: ServerMode): boolean {
return canonicalizeServerMode(mode) === 'docker';
}
export function isApiServerMode(mode?: ServerMode): boolean {
return canonicalizeServerMode(mode) === 'api';
}
export function serverModeLabel(mode?: ServerMode): string {
return canonicalizeServerMode(mode);
}
/** /**
* Configuration for a single agent in multi-agent mode. * Configuration for a single agent in multi-agent mode.
* Each agent has its own name, channels, and features. * Each agent has its own name, channels, and features.
@@ -50,11 +69,12 @@ export interface AgentConfig {
export interface LettaBotConfig { export interface LettaBotConfig {
// Server connection // Server connection
server: { server: {
// 'cloud' (api.letta.com) or 'selfhosted' // Canonical values: 'api' or 'docker'
mode: 'cloud' | 'selfhosted'; // Legacy aliases accepted for compatibility: 'cloud', 'selfhosted'
// Only for selfhosted mode mode: ServerMode;
// Only for docker mode
baseUrl?: string; baseUrl?: string;
// Only for cloud mode // Only for api mode
apiKey?: string; apiKey?: string;
}; };
@@ -71,7 +91,7 @@ export interface LettaBotConfig {
model?: string; model?: string;
}; };
// BYOK providers (cloud mode only) // BYOK providers (api mode only)
providers?: ProviderConfig[]; providers?: ProviderConfig[];
// Channel configurations // Channel configurations
@@ -272,7 +292,7 @@ export interface GoogleConfig {
// Default config // Default config
export const DEFAULT_CONFIG: LettaBotConfig = { export const DEFAULT_CONFIG: LettaBotConfig = {
server: { server: {
mode: 'cloud', mode: 'api',
}, },
agent: { agent: {
name: 'LettaBot', name: 'LettaBot',

View File

@@ -14,16 +14,23 @@ import { createApiServer } from './api/server.js';
import { loadOrGenerateApiKey } from './api/auth.js'; import { loadOrGenerateApiKey } from './api/auth.js';
// Load YAML config and apply to process.env (overrides .env values) // Load YAML config and apply to process.env (overrides .env values)
import { loadConfig, applyConfigToEnv, syncProviders, resolveConfigPath } from './config/index.js'; import {
import { isLettaCloudUrl } from './utils/server.js'; loadConfig,
applyConfigToEnv,
syncProviders,
resolveConfigPath,
isDockerServerMode,
serverModeLabel,
} from './config/index.js';
import { isLettaApiUrl } from './utils/server.js';
import { getDataDir, getWorkingDir, hasRailwayVolume } from './utils/paths.js'; import { getDataDir, getWorkingDir, hasRailwayVolume } from './utils/paths.js';
const yamlConfig = loadConfig(); const yamlConfig = loadConfig();
const configSource = existsSync(resolveConfigPath()) ? resolveConfigPath() : 'defaults + environment variables'; const configSource = existsSync(resolveConfigPath()) ? resolveConfigPath() : 'defaults + environment variables';
console.log(`[Config] Loaded from ${configSource}`); console.log(`[Config] Loaded from ${configSource}`);
if (yamlConfig.agents?.length) { if (yamlConfig.agents?.length) {
console.log(`[Config] Mode: ${yamlConfig.server.mode}, Agents: ${yamlConfig.agents.map(a => a.name).join(', ')}`); console.log(`[Config] Mode: ${serverModeLabel(yamlConfig.server.mode)}, Agents: ${yamlConfig.agents.map(a => a.name).join(', ')}`);
} else { } else {
console.log(`[Config] Mode: ${yamlConfig.server.mode}, Agent: ${yamlConfig.agent.name}`); console.log(`[Config] Mode: ${serverModeLabel(yamlConfig.server.mode)}, Agent: ${yamlConfig.agent.name}`);
} }
if (yamlConfig.agent?.model) { if (yamlConfig.agent?.model) {
console.warn('[Config] WARNING: agent.model in lettabot.yaml is deprecated and ignored. Use `lettabot model set <handle>` instead.'); console.warn('[Config] WARNING: agent.model in lettabot.yaml is deprecated and ignored. Use `lettabot model set <handle>` instead.');
@@ -98,8 +105,8 @@ async function refreshTokensIfNeeded(): Promise<void> {
return; return;
} }
// OAuth tokens only work with Letta Cloud - skip if using custom server // OAuth tokens only work with Letta API - skip if using custom server
if (!isLettaCloudUrl(process.env.LETTA_BASE_URL)) { if (!isLettaApiUrl(process.env.LETTA_BASE_URL)) {
return; return;
} }
@@ -435,11 +442,11 @@ const globalConfig = {
cronEnabled: process.env.CRON_ENABLED === 'true', // Legacy env var fallback cronEnabled: process.env.CRON_ENABLED === 'true', // Legacy env var fallback
}; };
// Validate LETTA_API_KEY is set for cloud mode (selfhosted mode doesn't require it) // Validate LETTA_API_KEY is set for API mode (docker mode doesn't require it)
if (yamlConfig.server.mode !== 'selfhosted' && !process.env.LETTA_API_KEY) { if (!isDockerServerMode(yamlConfig.server.mode) && !process.env.LETTA_API_KEY) {
console.error('\n Error: LETTA_API_KEY is required for Letta Cloud.'); console.error('\n Error: LETTA_API_KEY is required for Letta API.');
console.error(' Get your API key from https://app.letta.com and set it as an environment variable.'); console.error(' Get your API key from https://app.letta.com and set it as an environment variable.');
console.error(' Or use selfhosted mode: run "lettabot onboard" and select "Enter self-hosted URL".\n'); console.error(' Or use docker mode: run "lettabot onboard" and select "Enter Docker server URL".\n');
process.exit(1); process.exit(1);
} }

View File

@@ -6,9 +6,9 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
import * as p from '@clack/prompts'; import * as p from '@clack/prompts';
import { saveConfig, syncProviders } from './config/index.js'; import { saveConfig, syncProviders, isApiServerMode } from './config/index.js';
import type { AgentConfig, LettaBotConfig, ProviderConfig } from './config/types.js'; import type { AgentConfig, LettaBotConfig, ProviderConfig } from './config/types.js';
import { isLettaCloudUrl } from './utils/server.js'; import { isLettaApiUrl } from './utils/server.js';
import { CHANNELS, getChannelHint, isSignalCliInstalled, setupTelegram, setupSlack, setupDiscord, setupWhatsApp, setupSignal } from './channels/setup.js'; import { CHANNELS, getChannelHint, isSignalCliInstalled, setupTelegram, setupSlack, setupDiscord, setupWhatsApp, setupSignal } from './channels/setup.js';
// ============================================================================ // ============================================================================
@@ -118,7 +118,7 @@ async function saveConfigFromEnv(config: any, configPath: string): Promise<void>
const lettabotConfig: Partial<LettaBotConfig> & Pick<LettaBotConfig, 'server'> = { const lettabotConfig: Partial<LettaBotConfig> & Pick<LettaBotConfig, 'server'> = {
server: { server: {
mode: isLettaCloudUrl(config.baseUrl) ? 'cloud' : 'selfhosted', mode: isLettaApiUrl(config.baseUrl) ? 'api' : 'docker',
baseUrl: config.baseUrl, baseUrl: config.baseUrl,
apiKey: config.apiKey, apiKey: config.apiKey,
}, },
@@ -207,7 +207,7 @@ async function saveConfigFromEnv(config: any, configPath: string): Promise<void>
interface OnboardConfig { interface OnboardConfig {
// Auth // Auth
authMethod: 'keep' | 'oauth' | 'apikey' | 'selfhosted' | 'skip'; authMethod: 'keep' | 'oauth' | 'apikey' | 'docker' | 'selfhosted' | 'skip';
apiKey?: string; apiKey?: string;
baseUrl?: string; baseUrl?: string;
billingTier?: string; billingTier?: string;
@@ -288,6 +288,7 @@ interface OnboardConfig {
} }
const isPlaceholder = (val?: string) => !val || /^(your_|sk-\.\.\.|placeholder|example)/i.test(val); const isPlaceholder = (val?: string) => !val || /^(your_|sk-\.\.\.|placeholder|example)/i.test(val);
const isDockerAuthMethod = (method: OnboardConfig['authMethod']) => method === 'docker' || method === 'selfhosted';
// ============================================================================ // ============================================================================
// Step Functions // Step Functions
@@ -298,12 +299,12 @@ async function stepAuth(config: OnboardConfig, env: Record<string, string>): Pro
const { saveTokens, loadTokens, getOrCreateDeviceId, getDeviceName } = await import('./auth/tokens.js'); const { saveTokens, loadTokens, getOrCreateDeviceId, getDeviceName } = await import('./auth/tokens.js');
const baseUrl = config.baseUrl || env.LETTA_BASE_URL || process.env.LETTA_BASE_URL; const baseUrl = config.baseUrl || env.LETTA_BASE_URL || process.env.LETTA_BASE_URL;
const isLettaCloud = isLettaCloudUrl(baseUrl); const isLettaApi = isLettaApiUrl(baseUrl);
const existingTokens = loadTokens(); const existingTokens = loadTokens();
// Check both env and config for existing API key // Check both env and config for existing API key
const realApiKey = config.apiKey || (isPlaceholder(env.LETTA_API_KEY) ? undefined : env.LETTA_API_KEY); const realApiKey = config.apiKey || (isPlaceholder(env.LETTA_API_KEY) ? undefined : env.LETTA_API_KEY);
const validOAuthToken = isLettaCloud ? existingTokens?.accessToken : undefined; const validOAuthToken = isLettaApi ? existingTokens?.accessToken : undefined;
const hasExistingAuth = !!realApiKey || !!validOAuthToken; const hasExistingAuth = !!realApiKey || !!validOAuthToken;
const displayKey = realApiKey || validOAuthToken; const displayKey = realApiKey || validOAuthToken;
@@ -316,9 +317,9 @@ async function stepAuth(config: OnboardConfig, env: Record<string, string>): Pro
const authOptions = [ const authOptions = [
...(hasExistingAuth ? [{ value: 'keep', label: getAuthLabel(), hint: displayKey?.slice(0, 20) + '...' }] : []), ...(hasExistingAuth ? [{ value: 'keep', label: getAuthLabel(), hint: displayKey?.slice(0, 20) + '...' }] : []),
...(isLettaCloud ? [{ value: 'oauth', label: 'Login to Letta Platform', hint: 'Opens browser' }] : []), ...(isLettaApi ? [{ value: 'oauth', label: 'Login to Letta Platform', hint: 'Opens browser' }] : []),
{ value: 'apikey', label: 'Enter API Key manually', hint: 'Paste your key' }, { value: 'apikey', label: 'Enter API Key manually', hint: 'Paste your key' },
{ value: 'selfhosted', label: 'Enter self-hosted URL', hint: 'Local Letta server' }, { value: 'docker', label: 'Enter Docker server URL', hint: 'Local/custom Letta server' },
{ value: 'skip', label: 'Skip', hint: 'Continue without auth' }, { value: 'skip', label: 'Skip', hint: 'Continue without auth' },
]; ];
@@ -390,7 +391,7 @@ async function stepAuth(config: OnboardConfig, env: Record<string, string>): Pro
config.apiKey = apiKey; config.apiKey = apiKey;
env.LETTA_API_KEY = apiKey; env.LETTA_API_KEY = apiKey;
} }
} else if (authMethod === 'selfhosted') { } else if (authMethod === 'docker' || authMethod === 'selfhosted') {
const serverUrl = await p.text({ const serverUrl = await p.text({
message: 'Letta server URL', message: 'Letta server URL',
placeholder: 'http://localhost:8283', placeholder: 'http://localhost:8283',
@@ -403,7 +404,7 @@ async function stepAuth(config: OnboardConfig, env: Record<string, string>): Pro
env.LETTA_BASE_URL = url; env.LETTA_BASE_URL = url;
process.env.LETTA_BASE_URL = url; // Set immediately so model listing works process.env.LETTA_BASE_URL = url; // Set immediately so model listing works
// Clear any cloud API key since we're using self-hosted // Clear API key since we're using a Docker/custom server.
delete env.LETTA_API_KEY; delete env.LETTA_API_KEY;
delete process.env.LETTA_API_KEY; delete process.env.LETTA_API_KEY;
} else if (authMethod === 'keep') { } else if (authMethod === 'keep') {
@@ -458,14 +459,14 @@ async function stepAuth(config: OnboardConfig, env: Record<string, string>): Pro
} }
const spinner = p.spinner(); const spinner = p.spinner();
const serverLabel = config.baseUrl || 'Letta Cloud'; const serverLabel = config.baseUrl || 'Letta API';
spinner.start(`Checking connection to ${serverLabel}...`); spinner.start(`Checking connection to ${serverLabel}...`);
try { try {
const { testConnection } = await import('./tools/letta-api.js'); const { testConnection } = await import('./tools/letta-api.js');
const ok = await testConnection(); const ok = await testConnection();
spinner.stop(ok ? `Connected to ${serverLabel}` : 'Connection issue'); spinner.stop(ok ? `Connected to ${serverLabel}` : 'Connection issue');
if (!ok && config.authMethod === 'selfhosted') { if (!ok && isDockerAuthMethod(config.authMethod)) {
p.log.warn(`Could not connect to ${config.baseUrl}. Make sure the server is running.`); p.log.warn(`Could not connect to ${config.baseUrl}. Make sure the server is running.`);
} }
} catch { } catch {
@@ -561,8 +562,8 @@ const BYOK_PROVIDERS = [
]; ];
async function stepProviders(config: OnboardConfig, env: Record<string, string>): Promise<void> { async function stepProviders(config: OnboardConfig, env: Record<string, string>): Promise<void> {
// Only for free tier users on Letta Cloud (not self-hosted, not paid) // Only for free tier users on Letta API (not Docker/custom servers, not paid)
if (config.authMethod === 'selfhosted') return; if (isDockerAuthMethod(config.authMethod)) return;
if (config.billingTier !== 'free') return; if (config.billingTier !== 'free') return;
const selectedProviders = await p.multiselect({ const selectedProviders = await p.multiselect({
@@ -680,10 +681,10 @@ async function stepModel(config: OnboardConfig, env: Record<string, string>): Pr
const spinner = p.spinner(); const spinner = p.spinner();
// Determine if self-hosted (not Letta Cloud) // Determine if Docker/custom server (not Letta API)
const isSelfHosted = config.authMethod === 'selfhosted'; const isSelfHosted = isDockerAuthMethod(config.authMethod);
// Fetch billing tier for Letta Cloud users (if not already fetched) // Fetch billing tier for Letta API users (if not already fetched)
let billingTier: string | null = config.billingTier || null; let billingTier: string | null = config.billingTier || null;
if (!isSelfHosted && !billingTier) { if (!isSelfHosted && !billingTier) {
spinner.start('Checking account...'); spinner.start('Checking account...');
@@ -1155,7 +1156,8 @@ function showSummary(config: OnboardConfig): void {
keep: 'Keep existing', keep: 'Keep existing',
oauth: 'OAuth login', oauth: 'OAuth login',
apikey: config.apiKey ? `API Key (${config.apiKey.slice(0, 10)}...)` : 'API Key', apikey: config.apiKey ? `API Key (${config.apiKey.slice(0, 10)}...)` : 'API Key',
selfhosted: config.baseUrl ? `Self-hosted (${config.baseUrl})` : 'Self-hosted', docker: config.baseUrl ? `Docker server (${config.baseUrl})` : 'Docker server',
selfhosted: config.baseUrl ? `Docker server (${config.baseUrl})` : 'Docker server',
skip: 'None', skip: 'None',
}[config.authMethod]; }[config.authMethod];
lines.push(`Auth: ${authLabel}`); lines.push(`Auth: ${authLabel}`);
@@ -1320,12 +1322,12 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise<v
console.log(''); console.log('');
// Validate required fields // Validate required fields
if (!config.apiKey && isLettaCloudUrl(config.baseUrl)) { if (!config.apiKey && isLettaApiUrl(config.baseUrl)) {
console.error('❌ Error: LETTA_API_KEY is required'); console.error('❌ Error: LETTA_API_KEY is required');
console.error(' Get your API key from: https://app.letta.com/settings'); console.error(' Get your API key from: https://app.letta.com/settings');
console.error(' Then run: export LETTA_API_KEY="letta_..."'); console.error(' Then run: export LETTA_API_KEY="letta_..."');
console.error(''); console.error('');
console.error(' Or use self-hosted Letta:'); console.error(' Or use a Docker server:');
console.error(' export LETTA_BASE_URL="http://localhost:8283"'); console.error(' export LETTA_BASE_URL="http://localhost:8283"');
process.exit(1); process.exit(1);
} }
@@ -1385,8 +1387,8 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise<v
// Pre-populate from existing config // Pre-populate from existing config
const baseUrl = existingConfig.server.baseUrl || process.env.LETTA_BASE_URL || 'https://api.letta.com'; const baseUrl = existingConfig.server.baseUrl || process.env.LETTA_BASE_URL || 'https://api.letta.com';
const isLocal = !isLettaCloudUrl(baseUrl); const isLocal = !isLettaApiUrl(baseUrl);
p.note(`${baseUrl}\n${isLocal ? 'Self-hosted' : 'Letta Cloud'}`, 'Server'); p.note(`${baseUrl}\n${isLocal ? 'Docker server' : 'Letta API'}`, 'Server');
// Test server connection // Test server connection
const spinner = p.spinner(); const spinner = p.spinner();
@@ -1479,8 +1481,8 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise<v
await stepAuth(config, env); await stepAuth(config, env);
await stepAgent(config, env); await stepAgent(config, env);
// Fetch billing tier for free plan detection (only for Letta Cloud) // Fetch billing tier for free plan detection (only for Letta API)
if (config.authMethod !== 'selfhosted' && config.agentChoice === 'new') { if (!isDockerAuthMethod(config.authMethod) && config.agentChoice === 'new') {
const { getBillingTier } = await import('./utils/model-selection.js'); const { getBillingTier } = await import('./utils/model-selection.js');
const spinner = p.spinner(); const spinner = p.spinner();
spinner.start('Checking account...'); spinner.start('Checking account...');
@@ -1762,8 +1764,8 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise<v
// Convert to YAML config (multi-agent format) // Convert to YAML config (multi-agent format)
const yamlConfig: Partial<LettaBotConfig> & Pick<LettaBotConfig, 'server'> = { const yamlConfig: Partial<LettaBotConfig> & Pick<LettaBotConfig, 'server'> = {
server: { server: {
mode: config.authMethod === 'selfhosted' ? 'selfhosted' : 'cloud', mode: isDockerAuthMethod(config.authMethod) ? 'docker' : 'api',
...(config.authMethod === 'selfhosted' && config.baseUrl ? { baseUrl: config.baseUrl } : {}), ...(isDockerAuthMethod(config.authMethod) && config.baseUrl ? { baseUrl: config.baseUrl } : {}),
...(config.apiKey ? { apiKey: config.apiKey } : {}), ...(config.apiKey ? { apiKey: config.apiKey } : {}),
}, },
agents: [agentConfig], agents: [agentConfig],
@@ -1789,10 +1791,10 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise<v
saveConfig(yamlConfig, savePath); saveConfig(yamlConfig, savePath);
p.log.success('Configuration saved to lettabot.yaml'); p.log.success('Configuration saved to lettabot.yaml');
// Sync BYOK providers to Letta Cloud // Sync BYOK providers to Letta API.
if (yamlConfig.providers && yamlConfig.providers.length > 0 && yamlConfig.server.mode === 'cloud') { if (yamlConfig.providers && yamlConfig.providers.length > 0 && isApiServerMode(yamlConfig.server.mode)) {
const spinner = p.spinner(); const spinner = p.spinner();
spinner.start('Syncing BYOK providers to Letta Cloud...'); spinner.start('Syncing BYOK providers to Letta API...');
try { try {
await syncProviders(yamlConfig); await syncProviders(yamlConfig);
spinner.stop('BYOK providers synced'); spinner.stop('BYOK providers synced');

View File

@@ -26,11 +26,11 @@ export interface ModelInfo {
* Uses /v1/metadata/balance endpoint (same as letta-code) * Uses /v1/metadata/balance endpoint (same as letta-code)
* *
* @param apiKey - The API key to use * @param apiKey - The API key to use
* @param isSelfHosted - If true, skip billing check (self-hosted has no tiers) * @param isSelfHosted - If true, skip billing check (Docker/custom servers have no tiers)
*/ */
export async function getBillingTier(apiKey?: string, isSelfHosted?: boolean): Promise<string | null> { export async function getBillingTier(apiKey?: string, isSelfHosted?: boolean): Promise<string | null> {
try { try {
// Self-hosted servers don't have billing tiers // Docker/custom servers don't have billing tiers.
if (isSelfHosted) { if (isSelfHosted) {
return null; return null;
} }
@@ -39,7 +39,7 @@ export async function getBillingTier(apiKey?: string, isSelfHosted?: boolean): P
return 'free'; return 'free';
} }
// Always use Letta Cloud for billing check (not process.env.LETTA_BASE_URL) // Always use Letta API for billing check (not process.env.LETTA_BASE_URL)
const response = await fetch('https://api.letta.com/v1/metadata/balance', { const response = await fetch('https://api.letta.com/v1/metadata/balance', {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -111,7 +111,7 @@ async function fetchByokModels(apiKey?: string): Promise<ByokModel[]> {
* *
* For free users: Show free models first, then BYOK models from API * For free users: Show free models first, then BYOK models from API
* For paid users: Show featured models first, then all models * For paid users: Show featured models first, then all models
* For self-hosted: Fetch models from server * For Docker/custom servers: fetch models from server
*/ */
export async function buildModelOptions(options?: { export async function buildModelOptions(options?: {
billingTier?: string | null; billingTier?: string | null;
@@ -122,7 +122,7 @@ export async function buildModelOptions(options?: {
const isSelfHosted = options?.isSelfHosted; const isSelfHosted = options?.isSelfHosted;
const isFreeTier = billingTier?.toLowerCase() === 'free'; const isFreeTier = billingTier?.toLowerCase() === 'free';
// For self-hosted servers, fetch models from server // For Docker/custom servers, fetch models from server
if (isSelfHosted) { if (isSelfHosted) {
return buildServerModelOptions(); return buildServerModelOptions();
} }
@@ -182,7 +182,7 @@ export async function buildModelOptions(options?: {
} }
/** /**
* Build model options from self-hosted server * Build model options from Docker/custom server
*/ */
async function buildServerModelOptions(): Promise<Array<{ value: string; label: string; hint: string }>> { async function buildServerModelOptions(): Promise<Array<{ value: string; label: string; hint: string }>> {
const { listModels } = await import('../tools/letta-api.js'); const { listModels } = await import('../tools/letta-api.js');

View File

@@ -1,45 +1,50 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { isLettaCloudUrl } from './server.js'; import { isLettaApiUrl, isLettaCloudUrl } from './server.js';
describe('isLettaCloudUrl', () => { describe('isLettaApiUrl', () => {
it('returns true for undefined (default is cloud)', () => { it('returns true for undefined (default is Letta API)', () => {
expect(isLettaCloudUrl(undefined)).toBe(true); expect(isLettaApiUrl(undefined)).toBe(true);
}); });
it('returns true for Letta Cloud URL', () => { it('returns true for Letta API URL', () => {
expect(isLettaCloudUrl('https://api.letta.com')).toBe(true); expect(isLettaApiUrl('https://api.letta.com')).toBe(true);
}); });
it('returns true for Letta Cloud URL with trailing slash', () => { it('returns true for Letta API URL with trailing slash', () => {
expect(isLettaCloudUrl('https://api.letta.com/')).toBe(true); expect(isLettaApiUrl('https://api.letta.com/')).toBe(true);
}); });
it('returns true for Letta Cloud URL with path', () => { it('returns true for Letta API URL with path', () => {
expect(isLettaCloudUrl('https://api.letta.com/v1/agents')).toBe(true); expect(isLettaApiUrl('https://api.letta.com/v1/agents')).toBe(true);
}); });
it('returns false for localhost', () => { it('returns false for localhost', () => {
expect(isLettaCloudUrl('http://localhost:8283')).toBe(false); expect(isLettaApiUrl('http://localhost:8283')).toBe(false);
}); });
it('returns false for 127.0.0.1', () => { it('returns false for 127.0.0.1', () => {
expect(isLettaCloudUrl('http://127.0.0.1:8283')).toBe(false); expect(isLettaApiUrl('http://127.0.0.1:8283')).toBe(false);
}); });
it('returns false for custom server', () => { it('returns false for custom server', () => {
expect(isLettaCloudUrl('https://custom.server.com')).toBe(false); expect(isLettaApiUrl('https://custom.server.com')).toBe(false);
}); });
it('returns false for docker network URL', () => { it('returns false for docker network URL', () => {
expect(isLettaCloudUrl('http://letta:8283')).toBe(false); expect(isLettaApiUrl('http://letta:8283')).toBe(false);
}); });
it('returns false for invalid URL', () => { it('returns false for invalid URL', () => {
expect(isLettaCloudUrl('not-a-url')).toBe(false); expect(isLettaApiUrl('not-a-url')).toBe(false);
}); });
it('returns true for empty string (treated as default)', () => { it('returns true for empty string (treated as default)', () => {
// Empty string is falsy, so it's treated like undefined (default to cloud) // Empty string is falsy, so it's treated like undefined (default to Letta API)
expect(isLettaCloudUrl('')).toBe(true); expect(isLettaApiUrl('')).toBe(true);
});
it('keeps backward-compatible alias behavior', () => {
expect(isLettaCloudUrl('https://api.letta.com')).toBe(true);
expect(isLettaCloudUrl('http://localhost:8283')).toBe(false);
}); });
}); });

View File

@@ -1,24 +1,27 @@
/** /**
* Letta server URL utilities * Letta server URL utilities
* *
* The heuristic is simple: Letta Cloud lives at a known URL. * The heuristic is simple: Letta API lives at a known URL.
* Everything else is self-hosted. * Everything else is a Docker/custom server.
*/ */
import { LETTA_CLOUD_API_URL } from '../auth/oauth.js'; import { LETTA_API_URL } from '../auth/oauth.js';
/** /**
* Check if a URL points at Letta Cloud (api.letta.com) * Check if a URL points at Letta API (api.letta.com)
* *
* @param url - The base URL to check. When absent, assumes cloud (the default). * @param url - The base URL to check. When absent, assumes Letta API (the default).
*/ */
export function isLettaCloudUrl(url?: string): boolean { export function isLettaApiUrl(url?: string): boolean {
if (!url) return true; // no URL means the default (cloud) if (!url) return true; // no URL means the default (Letta API)
try { try {
const given = new URL(url); const given = new URL(url);
const cloud = new URL(LETTA_CLOUD_API_URL); const api = new URL(LETTA_API_URL);
return given.hostname === cloud.hostname; return given.hostname === api.hostname;
} catch { } catch {
return false; return false;
} }
} }
// Backward-compatible alias.
export const isLettaCloudUrl = isLettaApiUrl;