fix: fix server terminology with mode aliases (#277)
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
6
SKILL.md
6
SKILL.md
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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 │
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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`, {
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
27
src/main.ts
27
src/main.ts
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user