This release represents a comprehensive transformation of the codebase from Codex to LLMX, enhanced with LiteLLM integration to support 100+ LLM providers through a unified API. ## Major Changes ### Phase 1: Repository & Infrastructure Setup - Established new repository structure and branching strategy - Created comprehensive project documentation (CLAUDE.md, LITELLM-SETUP.md) - Set up development environment and tooling configuration ### Phase 2: Rust Workspace Transformation - Renamed all Rust crates from `codex-*` to `llmx-*` (30+ crates) - Updated package names, binary names, and workspace members - Renamed core modules: codex.rs → llmx.rs, codex_delegate.rs → llmx_delegate.rs - Updated all internal references, imports, and type names - Renamed directories: codex-rs/ → llmx-rs/, codex-backend-openapi-models/ → llmx-backend-openapi-models/ - Fixed all Rust compilation errors after mass rename ### Phase 3: LiteLLM Integration - Integrated LiteLLM for multi-provider LLM support (Anthropic, OpenAI, Azure, Google AI, AWS Bedrock, etc.) - Implemented OpenAI-compatible Chat Completions API support - Added model family detection and provider-specific handling - Updated authentication to support LiteLLM API keys - Renamed environment variables: OPENAI_BASE_URL → LLMX_BASE_URL - Added LLMX_API_KEY for unified authentication - Enhanced error handling for Chat Completions API responses - Implemented fallback mechanisms between Responses API and Chat Completions API ### Phase 4: TypeScript/Node.js Components - Renamed npm package: @codex/codex-cli → @valknar/llmx - Updated TypeScript SDK to use new LLMX APIs and endpoints - Fixed all TypeScript compilation and linting errors - Updated SDK tests to support both API backends - Enhanced mock server to handle multiple API formats - Updated build scripts for cross-platform packaging ### Phase 5: Configuration & Documentation - Updated all configuration files to use LLMX naming - Rewrote README and documentation for LLMX branding - Updated config paths: ~/.codex/ → ~/.llmx/ - Added comprehensive LiteLLM setup guide - Updated all user-facing strings and help text - Created release plan and migration documentation ### Phase 6: Testing & Validation - Fixed all Rust tests for new naming scheme - Updated snapshot tests in TUI (36 frame files) - Fixed authentication storage tests - Updated Chat Completions payload and SSE tests - Fixed SDK tests for new API endpoints - Ensured compatibility with Claude Sonnet 4.5 model - Fixed test environment variables (LLMX_API_KEY, LLMX_BASE_URL) ### Phase 7: Build & Release Pipeline - Updated GitHub Actions workflows for LLMX binary names - Fixed rust-release.yml to reference llmx-rs/ instead of codex-rs/ - Updated CI/CD pipelines for new package names - Made Apple code signing optional in release workflow - Enhanced npm packaging resilience for partial platform builds - Added Windows sandbox support to workspace - Updated dotslash configuration for new binary names ### Phase 8: Final Polish - Renamed all assets (.github images, labels, templates) - Updated VSCode and DevContainer configurations - Fixed all clippy warnings and formatting issues - Applied cargo fmt and prettier formatting across codebase - Updated issue templates and pull request templates - Fixed all remaining UI text references ## Technical Details **Breaking Changes:** - Binary name changed from `codex` to `llmx` - Config directory changed from `~/.codex/` to `~/.llmx/` - Environment variables renamed (CODEX_* → LLMX_*) - npm package renamed to `@valknar/llmx` **New Features:** - Support for 100+ LLM providers via LiteLLM - Unified authentication with LLMX_API_KEY - Enhanced model provider detection and handling - Improved error handling and fallback mechanisms **Files Changed:** - 578 files modified across Rust, TypeScript, and documentation - 30+ Rust crates renamed and updated - Complete rebrand of UI, CLI, and documentation - All tests updated and passing **Dependencies:** - Updated Cargo.lock with new package names - Updated npm dependencies in llmx-cli - Enhanced OpenAPI models for LLMX backend This release establishes LLMX as a standalone project with comprehensive LiteLLM integration, maintaining full backward compatibility with existing functionality while opening support for a wide ecosystem of LLM providers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Sebastian Krüger <support@pivoine.art>
229 lines
6.4 KiB
Rust
229 lines
6.4 KiB
Rust
use std::path::Path;
|
|
|
|
use anyhow::Result;
|
|
use llmx_core::config::load_global_mcp_servers;
|
|
use llmx_core::config::types::McpServerTransportConfig;
|
|
use predicates::str::contains;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::TempDir;
|
|
|
|
fn llmx_command(llmx_home: &Path) -> Result<assert_cmd::Command> {
|
|
let mut cmd = assert_cmd::Command::cargo_bin("llmx")?;
|
|
cmd.env("LLMX_HOME", llmx_home);
|
|
Ok(cmd)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn add_and_remove_server_updates_global_config() -> Result<()> {
|
|
let llmx_home = TempDir::new()?;
|
|
|
|
let mut add_cmd = llmx_command(llmx_home.path())?;
|
|
add_cmd
|
|
.args(["mcp", "add", "docs", "--", "echo", "hello"])
|
|
.assert()
|
|
.success()
|
|
.stdout(contains("Added global MCP server 'docs'."));
|
|
|
|
let servers = load_global_mcp_servers(llmx_home.path()).await?;
|
|
assert_eq!(servers.len(), 1);
|
|
let docs = servers.get("docs").expect("server should exist");
|
|
match &docs.transport {
|
|
McpServerTransportConfig::Stdio {
|
|
command,
|
|
args,
|
|
env,
|
|
env_vars,
|
|
cwd,
|
|
} => {
|
|
assert_eq!(command, "echo");
|
|
assert_eq!(args, &vec!["hello".to_string()]);
|
|
assert!(env.is_none());
|
|
assert!(env_vars.is_empty());
|
|
assert!(cwd.is_none());
|
|
}
|
|
other => panic!("unexpected transport: {other:?}"),
|
|
}
|
|
assert!(docs.enabled);
|
|
|
|
let mut remove_cmd = llmx_command(llmx_home.path())?;
|
|
remove_cmd
|
|
.args(["mcp", "remove", "docs"])
|
|
.assert()
|
|
.success()
|
|
.stdout(contains("Removed global MCP server 'docs'."));
|
|
|
|
let servers = load_global_mcp_servers(llmx_home.path()).await?;
|
|
assert!(servers.is_empty());
|
|
|
|
let mut remove_again_cmd = llmx_command(llmx_home.path())?;
|
|
remove_again_cmd
|
|
.args(["mcp", "remove", "docs"])
|
|
.assert()
|
|
.success()
|
|
.stdout(contains("No MCP server named 'docs' found."));
|
|
|
|
let servers = load_global_mcp_servers(llmx_home.path()).await?;
|
|
assert!(servers.is_empty());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn add_with_env_preserves_key_order_and_values() -> Result<()> {
|
|
let llmx_home = TempDir::new()?;
|
|
|
|
let mut add_cmd = llmx_command(llmx_home.path())?;
|
|
add_cmd
|
|
.args([
|
|
"mcp",
|
|
"add",
|
|
"envy",
|
|
"--env",
|
|
"FOO=bar",
|
|
"--env",
|
|
"ALPHA=beta",
|
|
"--",
|
|
"python",
|
|
"server.py",
|
|
])
|
|
.assert()
|
|
.success();
|
|
|
|
let servers = load_global_mcp_servers(llmx_home.path()).await?;
|
|
let envy = servers.get("envy").expect("server should exist");
|
|
let env = match &envy.transport {
|
|
McpServerTransportConfig::Stdio { env: Some(env), .. } => env,
|
|
other => panic!("unexpected transport: {other:?}"),
|
|
};
|
|
|
|
assert_eq!(env.len(), 2);
|
|
assert_eq!(env.get("FOO"), Some(&"bar".to_string()));
|
|
assert_eq!(env.get("ALPHA"), Some(&"beta".to_string()));
|
|
assert!(envy.enabled);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn add_streamable_http_without_manual_token() -> Result<()> {
|
|
let llmx_home = TempDir::new()?;
|
|
|
|
let mut add_cmd = llmx_command(llmx_home.path())?;
|
|
add_cmd
|
|
.args(["mcp", "add", "github", "--url", "https://example.com/mcp"])
|
|
.assert()
|
|
.success();
|
|
|
|
let servers = load_global_mcp_servers(llmx_home.path()).await?;
|
|
let github = servers.get("github").expect("github server should exist");
|
|
match &github.transport {
|
|
McpServerTransportConfig::StreamableHttp {
|
|
url,
|
|
bearer_token_env_var,
|
|
http_headers,
|
|
env_http_headers,
|
|
} => {
|
|
assert_eq!(url, "https://example.com/mcp");
|
|
assert!(bearer_token_env_var.is_none());
|
|
assert!(http_headers.is_none());
|
|
assert!(env_http_headers.is_none());
|
|
}
|
|
other => panic!("unexpected transport: {other:?}"),
|
|
}
|
|
assert!(github.enabled);
|
|
|
|
assert!(!llmx_home.path().join(".credentials.json").exists());
|
|
assert!(!llmx_home.path().join(".env").exists());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn add_streamable_http_with_custom_env_var() -> Result<()> {
|
|
let llmx_home = TempDir::new()?;
|
|
|
|
let mut add_cmd = llmx_command(llmx_home.path())?;
|
|
add_cmd
|
|
.args([
|
|
"mcp",
|
|
"add",
|
|
"issues",
|
|
"--url",
|
|
"https://example.com/issues",
|
|
"--bearer-token-env-var",
|
|
"GITHUB_TOKEN",
|
|
])
|
|
.assert()
|
|
.success();
|
|
|
|
let servers = load_global_mcp_servers(llmx_home.path()).await?;
|
|
let issues = servers.get("issues").expect("issues server should exist");
|
|
match &issues.transport {
|
|
McpServerTransportConfig::StreamableHttp {
|
|
url,
|
|
bearer_token_env_var,
|
|
http_headers,
|
|
env_http_headers,
|
|
} => {
|
|
assert_eq!(url, "https://example.com/issues");
|
|
assert_eq!(bearer_token_env_var.as_deref(), Some("GITHUB_TOKEN"));
|
|
assert!(http_headers.is_none());
|
|
assert!(env_http_headers.is_none());
|
|
}
|
|
other => panic!("unexpected transport: {other:?}"),
|
|
}
|
|
assert!(issues.enabled);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn add_streamable_http_rejects_removed_flag() -> Result<()> {
|
|
let llmx_home = TempDir::new()?;
|
|
|
|
let mut add_cmd = llmx_command(llmx_home.path())?;
|
|
add_cmd
|
|
.args([
|
|
"mcp",
|
|
"add",
|
|
"github",
|
|
"--url",
|
|
"https://example.com/mcp",
|
|
"--with-bearer-token",
|
|
])
|
|
.assert()
|
|
.failure()
|
|
.stderr(contains("--with-bearer-token"));
|
|
|
|
let servers = load_global_mcp_servers(llmx_home.path()).await?;
|
|
assert!(servers.is_empty());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn add_cant_add_command_and_url() -> Result<()> {
|
|
let llmx_home = TempDir::new()?;
|
|
|
|
let mut add_cmd = llmx_command(llmx_home.path())?;
|
|
add_cmd
|
|
.args([
|
|
"mcp",
|
|
"add",
|
|
"github",
|
|
"--url",
|
|
"https://example.com/mcp",
|
|
"--command",
|
|
"--",
|
|
"echo",
|
|
"hello",
|
|
])
|
|
.assert()
|
|
.failure()
|
|
.stderr(contains("unexpected argument '--command' found"));
|
|
|
|
let servers = load_global_mcp_servers(llmx_home.path()).await?;
|
|
assert!(servers.is_empty());
|
|
|
|
Ok(())
|
|
}
|